Skip to main content

提供商检测

FIM One 使用 LiteLLM 作为通用适配器。core/model/openai_compatible.py 中的 _resolve_litellm_model() 函数将用户的 LLM_BASE_URL + LLM_MODEL 映射到带有提供商前缀的 LiteLLM 模型标识符。该前缀决定了 LiteLLM 如何路由请求 — 原生 API 协议(Anthropic Messages API、Gemini 等)或通用 OpenAI 兼容的 /v1/chat/completions 解析顺序:
  1. 显式提供商(来自数据库 ModelConfig.provider 字段)— 最高优先级。如果提供商与 URL 中的已知域匹配,则不返回 api_base(LiteLLM 原生路由)。否则,api_base 设置为中继 URL。
  2. 域名匹配 KNOWN_DOMAINS — 通过主机名识别官方 API 端点。
  3. URL 路径提示 PATH_PROVIDER_HINTS — 常见于 UniAPI 等中继平台,其中路径中的 /claude/anthropic 表示上游协议。
  4. 回退openai/ 前缀(通用 OpenAI 兼容)。
当提供商前缀是原生协议(anthropic、gemini 等)且 URL 不是官方端点时,LiteLLM 使用原生协议但将请求发送到中继的 api_base。这意味着提供商特定的行为 — 包括下面描述的 Bedrock 预填充问题 — 无论请求是发送到官方 API 还是通过中继,都会适用。
如果你的中继 URL 路径中包含 /claude,FIM One 会自动通过 Anthropic 的原生协议路由。这通常是正确的(更好的流式传输、思考支持),但意味着提供商特定的行为会适用 — 包括下面描述的 Bedrock 预填充问题。

tool_choice — 四种模式

tool_choice 参数通过 OpenAI 格式标准化。LiteLLM 在发送请求前将其转换为每个提供商的原生协议。 "auto" 和强制模式({"type":"function",...})之间的区别是 FIM One 中每个兼容性问题的关键。这两种模式由具有不同要求的完全不同的子系统使用。

tool_choice 的使用位置

两个子系统使用 tool_choice,它们以根本不同的方式使用它。

ReAct 引擎 — tool_choice=“auto”

ReAct 循环需要模型在每次迭代中做出决定:调用工具或给出最终答案。只有 "auto" 才有意义——模型可以自由选择生成 tool_calls 或文本内容。这与所有提供商、所有模型和所有模式(包括扩展思考)兼容。 ReAct 引擎在 abilities["tool_call"] = True 时使用原生函数调用(_run_native),否则回退到 JSON-in-content 模式(_run_json)。两种模式都使用 "auto"——区别在于工具是通过 tools 参数传递还是在系统提示中描述。详见 ReAct 引擎——双模式执行

structured_llm_call — tool_choice=forced

一次性结构化提取(模式注解、DAG 规划、计划分析)。强制模型调用特定的虚拟函数,保证结构化 JSON 输出。这是触发提供商特定错误的调用点。 structured_llm_call 实现了一个 3 级降级链: 关键设计差异:structured_llm_call 的回退是运行时——它动态尝试每个级别并捕获异常以进行回退。ReAct 引擎的模式选择是构建时——它在开始时检查一次 _native_mode_active 并为整个循环提交到一种模式。这意味着 structured_llm_call 可以透明地从提供商特定的 400 错误中恢复,而 ReAct 依赖于模式在前期被正确选择。

Bedrock预填陷阱

当为使用anthropic/前缀解析的模型传递response_format={"type":"json_object"}时,LiteLLM会在内部注入一条助手预填消息来模拟JSON模式。Anthropic Messages API没有原生的response_format参数,所以LiteLLM通过在助手内容前加一个开括号来近似实现:
这在Anthropic的直接API上可以正常工作。但是,较新的AWS Bedrock模型版本会拒绝任何最后一条消息具有role: "assistant"的对话——他们称之为”助手消息预填”并抛出错误:
仅当同时满足以下三个条件时才会出现此错误:
  1. 模型使用anthropic/前缀解析(通过域名匹配或URL路径提示)。
  2. 传递了response_format={"type":"json_object"}structured_llm_call中的json_mode代码路径)。
  3. 实际后端是AWS Bedrock(拒绝预填)。
通过OpenAI兼容端点的Bedrock? 如果你的Bedrock中继暴露了OpenAI兼容的/v1/chat/completions端点(AWS自己的OpenAI兼容网关或第三方代理),且URL路径包含/claude/anthropic,FIM One会使用openai/前缀解析。LiteLLM随后将后端视为标准OpenAI兼容服务器,直接传递response_format而不注入任何预填,服务器原生处理JSON约束。预填陷阱不适用——你无需设置json_mode_enabled=false
影响原生工具调用(tool_choice="auto"配合tools=参数)。预填注入仅在response_format时发生。ReAct智能体执行完全不受影响。
如果第1级(native_fc)和第2级(json_mode)在Bedrock上都失败,系统会在第3级(plain_text)恢复。下面描述的json_mode_enabled标志消除了浪费的第2级调用。

修复方案:json_mode_enabled

一个按模型的 json_mode_enabled 标志控制是否尝试 Level 2(json_mode):
  • 数据库配置的模型:在 Admin → Models → Advanced settings 中切换。该标志存储在 ModelProviderModel.json_mode_enabled 上(默认值 TRUE)。
  • 环境变量配置的模型:在环境中设置 LLM_JSON_MODE_ENABLED=false
  • 效果:禁用时,abilities["json_mode"] 返回 Falseresponse_format 永远不会被传递 → 无预填充 → Bedrock 正常工作。降级链变为 native_fc → plain_text,完全跳过注定失败的 json_mode 调用。
  • 无质量损失:模型仍然返回有效的 JSON,因为系统提示指示它这样做。plain_text 级别使用 extract_json() 从自由格式内容中解析 JSON,这在现代模型中工作可靠。

思维模型 + 强制 tool_choice

某些模型已永久启用扩展思维(思维链)。它们的 API 拒绝强制 tool_choice,因为强制特定函数调用与模型优先推理的自由度相矛盾:
Anthropic 在协议级别强制执行此约束,某些其他提供商(例如 Moonshot AI / Kimi K2.5)遵循相同模式。 对于 Anthropic 模型,structured_llm_call 通过在调用 native_fc 时传递 reasoning_effort=None 来自动处理此问题,禁用该特定调用的扩展思维。结构化输出调用需要架构合规性,而不是深度推理——在这里禁用思维既正确又有益(更低延迟、更低成本)。 但是,某些模型(例如 Kimi K2.5)的思维永久启用,无法从外部禁用。对于这些模型,native_fc 总是以 400 错误失败,在结构化调用前浪费约 10 秒延迟,然后降级链才会降级到 json_mode。

修复:tool_choice_enabled

一个针对每个模型的 tool_choice_enabled 标志控制是否尝试 Level 1(native_fc):
  • 数据库配置的模型:在 Admin → Models → Advanced → “Native Function Calling” 中切换。该标志存储在 ModelProviderModel.tool_choice_enabled 上(默认为 TRUE)。
  • ENV 配置的模型:在环境中设置 LLM_TOOL_CHOICE_ENABLED=false
  • 效果:禁用时,abilities["tool_choice"] 返回 False → 降级链从 Level 2(json_mode)或 Level 3(plain_text)开始,完全跳过 native_fc。这消除了不兼容模型每次结构化调用约 10 秒的性能损失。
  • ReAct 智能体不受影响tool_choice_enabled 仅控制 structured_llm_call 中的强制工具选择。ReAct 引擎使用 tool_choice="auto"(模型自由决定),无论此设置如何都适用于所有模型。
tool_choice_enabledtool_call 是独立的能力标志。tool_call(对于 OpenAICompatibleLLM 始终为 True)控制工具是否被传递给模型 — 禁用它会破坏 ReAct 智能体。tool_choice 仅控制是否尝试强制工具选择以进行结构化输出提取。
tool_choice="auto" 不受思考模式影响。ReAct 引擎专门使用 "auto",因此启用思考时智能体执行工作正常。
不要设置 abilities["tool_call"] = False 来避免此约束。这会禁用 ReAct 的 _run_native 模式(使用 tool_choice="auto" 且与思考配合良好),强制其进入不太可靠的 _run_json 模式。
**提供商迁移说明:**某些第三方中继会静默丢弃不支持的参数,如 reasoning_effortdrop_params=True),因此即使配置了思考也永远不会激活。迁移到正确支持思考的提供商(Bedrock、直接 Anthropic API)时,native_fc 中的 reasoning_effort=None 确保一致的行为。无需用户操作 — 结构化输出在所有提供商中的工作方式相同。

快速参考:什么在哪里有效

推荐的每个模型配置

tool_choice_enabledjson_mode_enabled 都可以在管理员 → 模型 → 高级设置中按模型切换。默认值(两者都为 TRUE)适用于大多数提供商。仅在遇到错误或不必要的延迟时进行调整。
何时更改: 如果在日志中看到 structured_llm_call: native_fc call raised 警告,随后成功进行 json_mode 提取,则该模型不受益于原生 FC。为该模型禁用”原生函数调用”以消除浪费的 API 调用(每个结构化输出请求约 10 秒)。
环境变量级别的覆盖 适用于通过环境变量配置的所有模型(不是管理员 UI):

推理工作量和思考配置

FIM One 公开了两个环境变量用于控制扩展思考/推理: 当设置了 reasoning_effort 且模型被解析为 anthropic/ 时,应用以下两个额外行为:
  1. 温度被强制设置为 1.0。 Bedrock 在启用思考时拒绝 temperature != 1.0。FIM One 会自动处理此问题 — 无需用户操作。
  2. GPT-5.x 与工具:当存在 tools 时,reasoning_effort 被静默丢弃,因为 GPT-5 /v1/chat/completions 端点拒绝此组合。这仅影响 ReAct 工具循环;缺少 tools 参数的 structured_llm_call 调用不受影响。

结构化输出的防御性解析

即使 native_fc 正常工作,结构化输出管道也包含一个防御性解析层,用于处理来自任何提供商或兼容性层的边界情况。 DAG 规划器的 _dict_to_steps 解析器处理三个常见的边界情况:
  1. 单个对象而非数组。 某些模型返回 {"steps": {"id": "1", "task": "..."}} (单个步骤对象)而不是 {"steps": [{"id": "1", "task": "..."}]} (数组)。解析器通过检查 idtask 键来检测这种情况,并将对象包装在列表中。
  2. 双重编码的 JSON 字符串。 当结构化输出降级到 json_mode(缺乏模式强制)时,某些提供商将 steps 值作为 JSON 字符串而非原生数组返回 — 例如 {"steps": "[{\"id\": \"1\", ...}]"}。这个字符串可能还包含字面换行符(来自模型的格式化),会破坏标准的 json.loads。解析器使用 extract_json_value() (包含 _repair_json_strings)来处理:
    • JSON 字符串值内的字面换行符
    • 无效的转义序列(常见于 LaTeX 或代码内容)
    • 来自兼容性层的其他序列化问题
  3. 缺少 steps 包装器。 模型可能返回单个步骤作为顶级对象,而没有 steps 包装键。解析器在根级别检测 idtask,并相应地进行包装。
在正常操作下,native_fc 返回正确结构化的工具调用参数,这些边界情况不会出现。防御性解析器作为安全网存在,用于自定义 BaseLLM 子类、异常的提供商行为,或结构化输出降级到 json_mode 或 plain_text 的回退场景。

提示词缓存(跨提供商)

FIM One 通过 cache_control 断点实现 Anthropic 的显式提示词缓存,同时通过提示词部分注册表从其他所有提供商的自动前缀缓存中受益。目标是实现一条单一的提示词组装路径,在所有提供商中工作,而无需按调用的提示词形状差异。

架构

fim_one.core.prompt 模块公开三个基础元素:
  • PromptSection — 一个命名片段,包含静态 content: str 或动态 content: Callable
  • PromptRegistry — 一个记忆化存储(静态片段渲染一次,动态片段每次调用重新渲染)
  • DYNAMIC_BOUNDARY — 一个哨兵标记,注册表在最后一个静态片段和第一个动态片段之间插入,以便调用者可以在缓存断点处分割渲染的提示词
ReAct 的系统提示词(JSON 模式、原生函数调用模式、综合)分为:
  • 静态前缀(~95% 的提示词)— 身份、核心指南、工具描述
  • 动态后缀 — 当前日期时间、每个请求的语言指令、交接上下文

能力检测

fim_one.core.prompt.caching.is_cache_capable(model_id) 当模型 id 包含以下任何内容时返回 Trueclaudeanthropicbedrock/anthropicvertex_ai/claude。这些提供商接收两个 role="system" 消息,第一个(静态)消息带有 cache_control: {"type": "ephemeral"} 所有其他提供商接收一个单一的连接系统消息,没有 cache_control 字段——这是必要的,因为非 Anthropic 端点要么拒绝该字段,要么静默丢弃它,通过某些中继发送它会导致 400 unknown parameter 错误。

跨提供商覆盖

PromptRegistry 通过自动前缀缓存为每个提供商”免费”提供优势 — 通过在调用中保持静态部分字节相同(当前日期时间位于动态后缀而非前缀),每个自动缓存提供商的哈希匹配并命中其缓存。这就是为什么即使在考虑 Anthropic 特定的 cache_control 之前,Registry 也是一个基础的无模型优势。

可观测性

每个 chat/* 响应的 done_payload 现在包括:
TurnProfiler 每轮发出一条结构化日志行:turn_cache summary | model=claude-sonnet-4-6 | read_tokens=1067 | create_tokens=0 | saved_input_tokens=961 (~90%)。这也充当中继诚实探针 — 如果你通过 API 中继路由,比较实际计费的输入 token 与 read_tokens 来检测中继是否剥离 cache_control 或保留 0.10× 折扣。 LLM 层不返回美元估计 — 定价和中继加价在上层应用,因此 LLM 层仅返回客观的 token 计数。

多轮缓存 ROI

在 Claude 4 ReAct 轮次上测量,使用默认智能体提示词: 一个包含 10 个工具的 10 次迭代 ReAct 运行,在第一次之后的每一轮可节省约 8,640 个输入 token(9 次缓存命中 × 1067 token × 90%)。Anthropic 对第一次调用的缓存写入收费 1.25 倍,因此损益平衡点在第二次调用——单次查询不会受益。

推理重放策略(无模型正确性)

扩展思考/推理块在不同提供商之间的行为不同。统一的序列化策略会破坏协议契约和自动前缀缓存。fim_one.core.prompt.reasoning.reasoning_replay_policy(model_id) 返回三个值之一,并在 OpenAICompatibleLLM._build_request_kwargs() 中控制 ChatMessage.to_openai_dict(replay_policy=...) 的行为。

三种策略

  • anthropic_thinking — Claude 系列(包括 anthropic/bedrock/anthropicvertex_ai/claude)。思考块必须附加 signature 进行重放;如果缺少或更改签名,Anthropic 会拒绝后续轮次。
  • informational_only — 发出 CoT 但不期望重放的模型:DeepSeek R1 / R1-Distill、Qwen QwQ、Gemini 2.x thinking、OpenAI o1 / o3 / o4。它们的文档明确说明”不要在消息历史中发送 reasoning_content”。即使发送:
    • 违反提供商合约(未来版本可能开始拒绝)
    • 静默失效其自动前缀缓存 — 消息字节在每个轮次变化,破坏哈希
  • unsupported — 不具备推理能力的模型(GPT-4o、GPT-4 Turbo、Gemini 1.5、Mistral、Llama)。没有 CoT 可重放;字段不应出现。此策略也是未知模型 ID 的安全默认值。

执行

所有策略评估都在一个地方进行(_build_request_kwargs)。ChatMessage.to_openai_dict(replay_policy=None) 保留了 A3 宽松的默认设置,以便不协调的调用者不会回退。跨提供商测试矩阵位于 tests/test_reasoning_replay_policy.py 中,反向断言证明非 Anthropic 请求不会泄露 reasoning_content

对于用户

特性和错误行为都是自动的 — 您无需配置任何内容。工作流影响:
  • 如果您在同一对话中在 Claude 和 DeepSeek 智能体之间切换,历史记录会完整保存思考块;在下一轮中,传出消息形状会根据当前模型进行调整。
  • 如果您使用代理 / 自定义 BaseLLM 子类,请确保其模型 id 是可识别的(包含其中一个片段),否则默认的 unsupported 策略将适用 — 这是安全的,但意味着异常代理后的 Claude 可能会丢失思考重放。将模型 id 片段添加到 _CACHE_CAPABLE_MODEL_FRAGMENTS(在 core/prompt/caching.py 中)和/或推理策略查找。

故障排除

“This model does not support assistant message prefill” Bedrock + json_mode。两种修复方法:(1)设置 LLM_JSON_MODE_ENABLED=false 或在管理员模型设置中禁用 JSON Mode;或(2)如果你的 Bedrock 提供商提供 OpenAI 兼容的 /v1/chat/completions 端点,切换到该端点——FIM One 将其解析为 openai/,预填充注入永远不会发生。 “Thinking may not be enabled when tool_choice forces tool use” / “tool_choice ‘specified’ is incompatible with thinking enabled” 对于 Anthropic 模型,structured_llm_call 会自动为 native_fc 调用禁用思考。对于其他始终启用思考的提供商(例如 Kimi K2.5),在模型的高级设置中禁用”Native Function Calling”,或全局设置 LLM_TOOL_CHOICE_ENABLED=false。降级链将跳过 native_fc,改为通过 json_mode 或 plain_text 提取结构化输出。 “DAG pipeline failed: LLM ‘steps’ is not an array” LLM 将 steps 字段返回为字符串或单个对象,而不是数组。这通常意味着结构化输出降级到了 json_mode(缺乏模式强制)。检查日志中的 structured_llm_call: level=xxx——如果显示 json_mode 而不是 native_fc,则 native_fc 正在无声地失败。如果使用自定义 BaseLLM 子类,请验证它接受 reasoning_effort 关键字参数。 ReAct 意外降级到 JSON mode 检查模型的 abilities["tool_call"] 是否为 True。对于 OpenAICompatibleLLM,这始终为 True,但自定义 BaseLLM 子类可能会覆盖它。使用管理员 API 中的模型详情端点进行验证。 structured_llm_call 耗尽所有级别并抛出 StructuredOutputError 模型在任何级别都无法生成可解析的 JSON。这在现代模型中很少见。检查:(1)模式是有效的 JSON Schema,(2)模型有足够的 max_tokens 来生成完整响应,(3)系统提示没有与模式指令相冲突。DAG 规划器和分析器都提供 default_value 回退,因此此错误仅从显式省略默认值的调用站点传播。