Liking cljdoc? Tell your friends :D

im.ttalk.agent.provider.common.base

OpenAI 兼容 Provider 基础模块

为所有 OpenAI 兼容的 LLM Provider 提供统一的基础实现。 支持: OpenAI, ZhiPu, Ollama, Gemini, Mistral 等。

主要功能:

  • 统一的 API 调用包装
  • 通用的 Provider Record 实现
  • 配置管理

使用示例:

(require '[im.ttalk.agent.provider.common.base :as base])

;; 创建配置 (def config (base/make-config :openai "https://api.openai.com/v1" "OPENAI_API_KEY"))

;; 创建 Provider (def provider (base/create-provider config))

OpenAI 兼容 Provider 基础模块

为所有 OpenAI 兼容的 LLM Provider 提供统一的基础实现。
支持: OpenAI, ZhiPu, Ollama, Gemini, Mistral 等。

主要功能:
- 统一的 API 调用包装
- 通用的 Provider Record 实现
- 配置管理

使用示例:

(require '[im.ttalk.agent.provider.common.base :as base])

;; 创建配置
(def config (base/make-config
              :openai
              "https://api.openai.com/v1"
              "OPENAI_API_KEY"))

;; 创建 Provider
(def provider (base/create-provider config))
raw docstring

im.ttalk.agent.provider.common.business-codes

厂商业务码 → canonical 错误类型的码表(designs/provider-reliability.md §1; 照搬 Keelson Providers/ProviderBusinessCodes.cs + ChatProviderException.RefineKind)。

为什么要有这张表:限流和欠费在 HTTP 上常常是同一个 429(OpenAI 兼容的 insufficient_quota 就这么干),光看状态码会把「再等一会就有」当成「再等一个月也没有」, ChatModel 于是对欠费反复退避重试。反过来,欠费被回成 400 时只看状态码会判成参数错。 状态码只给初判,业务码说了算。

判定次序(refine-error,只会「收紧」):

  1. 码表命中(error.type / error.code / 顶层 code / MiniMax base_resp.status_code)→ 定; provider 专属行优先于通用行;
  2. 402 → 欠费;
  3. 原判是 :validation-error / :provider-error 且文案像「上下文超长」→ :context-overflow-error;
  4. 原判是 auth / validation / rate-limit → 维持(文案绝不凭空造出限流,也不把限流改成欠费);
  5. 其余(5xx / provider)文案像欠费 → :quota-exhausted-error。

码表是数据(不是 if 链):补新样本往 rules 加一行即可,判定次序不用动。

厂商业务码 → canonical 错误类型的码表(designs/provider-reliability.md §1;
照搬 Keelson `Providers/ProviderBusinessCodes.cs` + `ChatProviderException.RefineKind`)。

**为什么要有这张表**:限流和欠费在 HTTP 上常常是同一个 429(OpenAI 兼容的
`insufficient_quota` 就这么干),光看状态码会把「再等一会就有」当成「再等一个月也没有」,
ChatModel 于是对欠费反复退避重试。反过来,欠费被回成 400 时只看状态码会判成参数错。
**状态码只给初判,业务码说了算。**

判定次序(`refine-error`,只会「收紧」):
1. 码表命中(`error.type` / `error.code` / 顶层 `code` / MiniMax `base_resp.status_code`)→ 定;
   provider 专属行优先于通用行;
2. 402 → 欠费;
3. 原判是 :validation-error / :provider-error 且文案像「上下文超长」→ :context-overflow-error;
4. 原判是 auth / validation / rate-limit → 维持(**文案绝不凭空造出限流,也不把限流改成欠费**);
5. 其余(5xx / provider)文案像欠费 → :quota-exhausted-error。

码表是数据(不是 if 链):补新样本往 `rules` 加一行即可,判定次序不用动。
raw docstring

im.ttalk.agent.provider.common.cache

Anthropic Prompt Caching 策略层(对标 Spring AI AnthropicCacheStrategy)

======================================== 背景

Anthropic 通过在内容块上挂 cache_control 来缓存请求前缀,命中后该段 输入 token 仅按约 0.1x 计费。要点(以官方为准):

  • 渲染顺序:tools -> system -> messages。cache_control 标记的是"断点" (breakpoint),断点之前的全部内容作为可缓存前缀。
  • 单请求最多 4 个 breakpoint。
  • 最小可缓存前缀与模型相关(Opus/Haiku4.5 = 4096 token,Sonnet4.x = 1024/2048); 过短不会缓存(无报错,cache_creation_input_tokens=0)。
  • 任意字节变化都会使该位置之后的缓存失效 —— 稳定内容靠前,易变内容靠后。

======================================== 策略(cache-strategy)

:none 不缓存(默认) :system 仅缓存 system :tools 仅缓存工具定义 :system-and-tools 缓存 system + tools(两个 breakpoint) :conversation 缓存到"当前用户问题之前"的对话历史 (标记最后一条消息的最后一个内容块) :tool-results 缓存到最后一个 tool_result(多轮工具循环里跨轮复用工具结果, 标记 messages 中最后一个 type=tool_result 的内容块) :system-and-conversation 同时缓存 system 与对话历史(两个 breakpoint)

ttl(:cache-ttl)两种形状:

  • 标量:nil -> 默认 5 分钟;"1h" -> 1 小时;本策略的所有断点同一档(老用法)。
  • 按断点给档的 map:键 :tools / :system / :conversation / :tool-results, 缺键或 nil = 5 分钟(cache_control 不带 ttl 字段)。典型: {:cache-strategy :system-and-conversation :cache-ttl {:system "1h" :conversation nil}} ⇒ 前缀(tools+system)1h、对话尾巴 5m——尾巴每轮被取代,付 1h 的 2× 写入费纯亏 (feedbacks/2026-11-10-provider-request-hook-and-two-tier-cache-ttl.md)。

服务端规则:长 TTL 的断点必须排在短 TTL 之前(渲染顺序 tools → system → messages)。 apply-anthropic-cache 在注入前校验,违者抛 ex-info(:type :invalid-cache-ttl), 不等服务端 400。任一断点为 "1h" 时 anthropic provider 自动并入 beta extended-cache-ttl-2025-04-11(兼容端点如 MiniMax /anthropic 少了它会静默退回 5m)。

用法见 apply-anthropic-cache。

Anthropic Prompt Caching 策略层(对标 Spring AI AnthropicCacheStrategy)

========================================
背景
========================================

Anthropic 通过在内容块上挂 `cache_control` 来缓存请求前缀,命中后该段
输入 token 仅按约 0.1x 计费。要点(以官方为准):

- 渲染顺序:tools -> system -> messages。`cache_control` 标记的是"断点"
  (breakpoint),断点之前的全部内容作为可缓存前缀。
- 单请求最多 4 个 breakpoint。
- 最小可缓存前缀与模型相关(Opus/Haiku4.5 = 4096 token,Sonnet4.x = 1024/2048);
  过短不会缓存(无报错,cache_creation_input_tokens=0)。
- 任意字节变化都会使该位置之后的缓存失效 —— 稳定内容靠前,易变内容靠后。

========================================
策略(cache-strategy)
========================================

:none              不缓存(默认)
:system            仅缓存 system
:tools             仅缓存工具定义
:system-and-tools  缓存 system + tools(两个 breakpoint)
:conversation      缓存到"当前用户问题之前"的对话历史
                   (标记最后一条消息的最后一个内容块)
:tool-results      缓存到最后一个 tool_result(多轮工具循环里跨轮复用工具结果,
                   标记 messages 中最后一个 type=tool_result 的内容块)
:system-and-conversation  同时缓存 system 与对话历史(两个 breakpoint)

ttl(:cache-ttl)两种形状:
- 标量:nil -> 默认 5 分钟;"1h" -> 1 小时;本策略的**所有**断点同一档(老用法)。
- 按断点给档的 map:键 :tools / :system / :conversation / :tool-results,
  缺键或 nil = 5 分钟(cache_control 不带 ttl 字段)。典型:
    {:cache-strategy :system-and-conversation
     :cache-ttl {:system "1h" :conversation nil}}
  ⇒ 前缀(tools+system)1h、对话尾巴 5m——尾巴每轮被取代,付 1h 的 2× 写入费纯亏
  (feedbacks/2026-11-10-provider-request-hook-and-two-tier-cache-ttl.md)。

服务端规则:**长 TTL 的断点必须排在短 TTL 之前**(渲染顺序 tools → system → messages)。
apply-anthropic-cache 在注入前校验,违者抛 ex-info(:type :invalid-cache-ttl),
不等服务端 400。任一断点为 "1h" 时 anthropic provider 自动并入 beta
`extended-cache-ttl-2025-04-11`(兼容端点如 MiniMax /anthropic 少了它会静默退回 5m)。

用法见 apply-anthropic-cache。
raw docstring

im.ttalk.agent.provider.common.embeddings

Embedding HTTP 调用与响应归一(两种线上形态)

  1. OpenAI 兼容 POST {base}/embeddings body {:model .. :input ["a" "b"] :dimensions n? :encoding_format ..} resp {:data [{:index 0 :embedding [..]} ..] :model .. :usage {:prompt_tokens ..}} 覆盖:OpenAI / 智谱 / SiliconFlow / Ollama / Gemini(兼容端点) / Mistral / xAI …

  2. DashScope 原生 POST .../services/embeddings/text-embedding/text-embedding body {:model .. :input {:texts [..]} :parameters {:dimension n :text_type ..}} resp {:output {:embeddings [{:text_index 0 :embedding [..]}]} :usage {:total_tokens n}}

两条路的公共约定:

  • 返回向量与入参 texts 同序等长(按服务端返回的 index 重排,不假定顺序)
  • 超批自动切片(:batch-size),usage 逐片累加
  • 失败一律抛规范错误(D5),与 chat 路径同一套词汇
Embedding HTTP 调用与响应归一(两种线上形态)

1. **OpenAI 兼容** `POST {base}/embeddings`
   body  {:model .. :input ["a" "b"] :dimensions n? :encoding_format ..}
   resp  {:data [{:index 0 :embedding [..]} ..] :model .. :usage {:prompt_tokens ..}}
   覆盖:OpenAI / 智谱 / SiliconFlow / Ollama / Gemini(兼容端点) / Mistral / xAI …

2. **DashScope 原生** `POST .../services/embeddings/text-embedding/text-embedding`
   body  {:model .. :input {:texts [..]} :parameters {:dimension n :text_type ..}}
   resp  {:output {:embeddings [{:text_index 0 :embedding [..]}]} :usage {:total_tokens n}}

两条路的公共约定:
- 返回向量与入参 texts **同序等长**(按服务端返回的 index 重排,不假定顺序)
- 超批自动切片(`:batch-size`),usage 逐片累加
- 失败一律抛规范错误(D5),与 chat 路径同一套词汇
raw docstring

im.ttalk.agent.provider.common.memo

轻量有界缓存工具。

与 clojure.core/memoize 的区别:容量有界(超限整体清空重建), 不会随参数多样性无限增长——适合做库代码里的转换缓存。

轻量有界缓存工具。

与 clojure.core/memoize 的区别:容量有界(超限整体清空重建),
不会随参数多样性无限增长——适合做库代码里的转换缓存。
raw docstring

im.ttalk.agent.provider.common.openai-compat

OpenAI 兼容 API 调用

提供 OpenAI 风格 API 的通用调用代码,供多个 Provider 复用:

  • OpenAI
  • 智谱 AI (ZhiPu)
  • Ollama
  • 其他 OpenAI 兼容 API

使用示例:

(require '[im.ttalk.agent.provider.common.openai-compat :as compat])

;; 同步调用 (compat/call-api api-url api-key config messages tools)

;; 流式调用 (compat/call-api-stream api-url api-key config messages tools on-token)

OpenAI 兼容 API 调用

提供 OpenAI 风格 API 的通用调用代码,供多个 Provider 复用:
- OpenAI
- 智谱 AI (ZhiPu)
- Ollama
- 其他 OpenAI 兼容 API

使用示例:

(require '[im.ttalk.agent.provider.common.openai-compat :as compat])

;; 同步调用
(compat/call-api api-url api-key config messages tools)

;; 流式调用
(compat/call-api-stream api-url api-key config messages tools on-token)
raw docstring

im.ttalk.agent.provider.common.rate-limit

统一限流快照(designs/provider-reliability.md §6;Keelson Providers/RateLimitInfo.cs)。

OpenAI 的 x-ratelimit-{limit,remaining,reset}-{requests,tokens} 与 Anthropic 的 anthropic-ratelimit-{requests,tokens}-{limit,remaining,reset} 折成同一形状:

{:requests-limit :requests-remaining :requests-reset :tokens-limit :tokens-remaining :tokens-reset}

头缺哪格就没有哪格;全空 → nil(不挂)。有 retry-after 头时另带 :retry-after (Anthropic 旧 parse-rate-limit 形状的超集,向后兼容)。

头值可以是字符串,也可以是 java.net.http 给的 List<String>(http.client 的 http-headers->map 曾经这么存)——取第一个值(im.ttalk.http/header)。

统一限流快照(designs/provider-reliability.md §6;Keelson `Providers/RateLimitInfo.cs`)。

OpenAI 的 `x-ratelimit-{limit,remaining,reset}-{requests,tokens}` 与 Anthropic 的
`anthropic-ratelimit-{requests,tokens}-{limit,remaining,reset}` 折成同一形状:

  {:requests-limit :requests-remaining :requests-reset
   :tokens-limit   :tokens-remaining   :tokens-reset}

头缺哪格就没有哪格;全空 → nil(不挂)。有 `retry-after` 头时另带 `:retry-after`
(Anthropic 旧 `parse-rate-limit` 形状的超集,向后兼容)。

头值可以是字符串,也可以是 `java.net.http` 给的 `List<String>`(`http.client` 的
`http-headers->map` 曾经这么存)——取第一个值(`im.ttalk.http/header`)。
raw docstring

im.ttalk.agent.provider.common.request-hook

宿主对最终请求体的最后一改:config :configure-request。

形状:(fn [payload] payload),payload 是本库组好的完整请求体(wire 形态的 map,键为 snake_case 关键字)——思考字段(common.thinking 生成的)、:extra-body、 缓存断点(Anthropic)、流式的 :stream true 都已在场,钩子看得见、删得掉、 改写得了;返回值原样作为请求体发出。

为什么要有(feedbacks/2026-11-10-provider-request-hook-and-two-tier-cache-ttl.md): :extra-body 只能 merge,删不掉库生成的键。qwen 收到 thinking / reasoning_effort 回 400(它要 enable_thinking)、kimi-k2 收到 temperature 回 400——宿主手里只有 config,算不出最终请求体,于是绕不过去。 对标 Keelson.Agent 的 ConfigureRequest(Action<JsonObject>)。

约定:

  • 同步 / 流式 / deferred 各条路径同一处调用,发出之前最后一步。
  • 返回非 map 视为宿主 bug,当场抛 ex-info(:type :invalid-request-hook), 不静默发出半截请求体。
  • 钩子抛出的异常原样上抛(不包装、不吞)。
宿主对**最终请求体**的最后一改:config `:configure-request`。

形状:`(fn [payload] payload)`,payload 是本库组好的完整请求体(wire 形态的
map,键为 snake_case 关键字)——思考字段(common.thinking 生成的)、`:extra-body`、
缓存断点(Anthropic)、流式的 `:stream true` **都已在场**,钩子看得见、删得掉、
改写得了;返回值原样作为请求体发出。

为什么要有(feedbacks/2026-11-10-provider-request-hook-and-two-tier-cache-ttl.md):
`:extra-body` 只能 merge,删不掉库生成的键。qwen 收到 `thinking` /
`reasoning_effort` 回 400(它要 `enable_thinking`)、kimi-k2 收到 `temperature`
回 400——宿主手里只有 config,算不出最终请求体,于是绕不过去。
对标 Keelson.Agent 的 `ConfigureRequest`(`Action<JsonObject>`)。

约定:
- 同步 / 流式 / deferred 各条路径**同一处**调用,发出之前最后一步。
- 返回非 map 视为宿主 bug,当场抛 `ex-info`(`:type :invalid-request-hook`),
  不静默发出半截请求体。
- 钩子抛出的异常原样上抛(不包装、不吞)。
raw docstring

im.ttalk.agent.provider.common.response-parser

响应解析模块

提供统一的 LLM API 响应解析功能,支持 OpenAI 格式的响应。 将 OpenAI 原始响应转换为统一的内部格式。

使用示例:

(require '[im.ttalk.agent.provider.common.response-parser :as parser])

;; 提取工具调用 (parser/extract-tool-calls response)

;; 提取文本 (parser/extract-text response)

;; 标准化响应为统一格式 (parser/normalize-response response)

响应解析模块

提供统一的 LLM API 响应解析功能,支持 OpenAI 格式的响应。
将 OpenAI 原始响应转换为统一的内部格式。

使用示例:

(require '[im.ttalk.agent.provider.common.response-parser :as parser])

;; 提取工具调用
(parser/extract-tool-calls response)

;; 提取文本
(parser/extract-text response)

;; 标准化响应为统一格式
(parser/normalize-response response)
raw docstring

im.ttalk.agent.provider.common.thinking

thinking 档位统一(designs/provider-reliability.md §7;Keelson Providers/ThinkingKeys.cs、 OpenAICompatibleChatClient.ApplyThinkingCall、AnthropicChatClient.ApplyThinkingCall)。

上层只说一句「档位 / 开关」,这里按各家风格生成 wire:

config 键含义
:thinking-effort档位名(字符串 / 关键字),原样上 wire(DeepSeek 的 "max" 也行);有档位即开
:thinking-enabled只开 / 关,不带档位(effort 在场时被忽略)
:thinking-styleOpenAI 兼容线的写法::plain / :nested / :top-level / :qwen / :none;缺省按 provider 预设
:thinking-budget-tokensAnthropic 旧式预算(压过档位与「只开」,只有「关」压得过它)
:thinking-always-on?Anthropic:关不掉思考的模型,「关」时省略字段

不破坏现有透传:config 里显式给了 :thinking(或 OpenAI 的 :reasoning-effort、Responses 的 :reasoning)→ 对应字段不由本 ns 生成,显式值原样发。两个新键都没给 → 什么都不生成。

thinking 档位统一(designs/provider-reliability.md §7;Keelson `Providers/ThinkingKeys.cs`、
`OpenAICompatibleChatClient.ApplyThinkingCall`、`AnthropicChatClient.ApplyThinkingCall`)。

上层只说一句「档位 / 开关」,这里按各家风格生成 wire:

| config 键 | 含义 |
|---|---|
| `:thinking-effort`  | 档位名(字符串 / 关键字),**原样上 wire**(DeepSeek 的 "max" 也行);有档位即开 |
| `:thinking-enabled` | 只开 / 关,不带档位(effort 在场时被忽略) |
| `:thinking-style`   | OpenAI 兼容线的写法:`:plain` / `:nested` / `:top-level` / `:qwen` / `:none`;缺省按 provider 预设 |
| `:thinking-budget-tokens` | Anthropic 旧式预算(压过档位与「只开」,只有「关」压得过它) |
| `:thinking-always-on?`    | Anthropic:关不掉思考的模型,「关」时省略字段 |

**不破坏现有透传**:config 里显式给了 `:thinking`(或 OpenAI 的 `:reasoning-effort`、Responses 的
`:reasoning`)→ 对应字段不由本 ns 生成,显式值原样发。两个新键都没给 → 什么都不生成。
raw docstring

cljdoc builds & hosts documentation for Clojure/Script libraries

Keyboard shortcuts
Ctrl+kJump to recent docs
←Move to previous article
→Move to next article
Ctrl+/Jump to the search field
× close