Terminus 2 手册

配置固化

stage-1Configuration Crystallization6 个函数

这个 Stage 在解决什么问题?它把用户传进来的零散配置,一次性定成一个稳定的 Agent 对象,让后面所有 Stage 都能假设“模型、解析器、模板、计数器、会话字段都已经就位”。它只在 Terminus2 实例创建时跑一次,不处理某个具体任务,而是先把“这台 Agent 机器”装好;如果这一步没定清,后面的 Environment Setup 就不知道该按什么规则建环境。

  1. 先读构造参数,并把兼容问题在这里一次处理完。

    Terminus2.__init__()(创建 Agent 实例)先接住用户给的约 25 个配置项。这里最重要的不是“存起来”,而是先把历史兼容和优先级讲清:比如旧参数 episodes 和新参数 max_episodes 同时出现时,到底听谁的。 这一步存在的原因很直接:配置冲突如果拖到运行中才暴露,后面每个 Stage 都要反复防御;在入口处定一次,后面就能按一个口径工作。

  2. 再把核心依赖真的准备好。

    然后它初始化 LLM(大语言模型)客户端,也就是后面真正拿去发请求的 Chat 封装;接着按 parser 名称选 parser(把模型输出转成 Agent 可用结果的解析器)。 这一步的意义是:后续 Stage 不该再关心“模型对象有没有建好”“解析器是哪一种”。它们只该直接使用。 这里也有明确的失败边界:如果模型信息无法解析、LLM 起不来,或 parser 名称不合法,本阶段就直接报错并停下,不进入下游。

  3. 再把提示词模板的位置定下来。

    它会决定 prompt template(提示词模板)和 timeout template(超时场景下用的模板)从哪里读。 为什么这一步要放在这里?因为模板其实也是配置的一部分。先把路径定死,后面运行任务时就不用再猜“该用哪套提示词”,也避免不同任务临时选模板造成行为漂移。

  4. 最后把所有“每次运行会变”的字段先放回默认值。

    比如轨迹步数、待处理 completion、待处理 subagent(子 Agent,被主 Agent 调用的小 Agent)引用、handoff prompt、asciinema 标记、子 Agent 指标、API 请求时间、总结次数、episode 计数、session id。 这些字段虽然是运行期状态,但它们的“初始空白值”必须在实例创建时就声明清楚。这样同一个 Agent 实例在第一次跑任务前,就是一个干净、可预测的起点。

  5. 成功后,才把这个“已经定型”的对象交给下游。

    到这一步,系统拿到的已经不是“用户给的一堆参数”,而是“一个状态完整、依赖齐全、默认值明确的 Agent”。下一个 Stage 可以直接开始建环境,而不用回头处理配置细节。

非寄存器对象字段(可验证的实例状态):

上游其实只有一件事:用户传进来的构造参数。这个 Stage 把它们压成稳定的对象状态;下游 Environment Setup 消费的不是原始参数,而是一个已经选好模型、解析器、模板并且带着默认运行字段的 Agent。若这里失败,流水线就在实例创建阶段结束,不会进入环境搭建。

函数细节6

Terminus2.__init__terminus_2.py:144–331 ↗

代理配置冻结与运行态字段初始化入口

stage 上下文: 该条目对应 stage-1 的首个构造单元:把构造参数与少量兼容性 kwargs 解析为实例上的 self._* 配置和默认运行态字段。触发条件就是外部直接调用 Terminus2(...)。本 stage 目前未给出其他兄弟函数;因此这里承担后续实例方法可依赖的初始配置基线。

这段代码在干什么

Terminus2.__init__ 接收日志目录、模型/解析器/采样/总结/轨迹/tmux/LLM 后端等参数,并把它们固化到实例字段中。函数会先校验 model_name,再解析模型信息、创建 self._llm、选择 self._parser、读取 prompt/timeout 模板文本,然后处理 max_turns 与兼容参数 kwargs['max_episodes']kwargs['episodes'] 的优先级。其真正产出不是返回值,而是对大量 self._* 字段的写入,以及在显式使用废弃或限流参数时通过 self.logger.warning(...) 发出告警。

接口 · 参数 / IO

(self, logs_dir: Path, model_name: str | None = None, max_turns: int | None = None, parser_name: str = "json", api_base: str | None = None, temperature: float | None = None, reasoning_effort: Literal["none", "minimal", "low", "medium", "high", "xhigh", "max", "default"] | None = None, collect_rollout_details: bool = False, session_id: str | None = None, enable_summarize: bool = True, proactive_summarization_threshold: int = 8000, max_thinking_tokens: int | None = None, model_info: dict | None = None, trajectory_config: TrajectoryConfig | None = None, tmux_pane_width: int = 160, tmux_pane_height: int = 40, store_all_messages: bool = False, record_terminal_session: bool = True, interleaved_thinking: bool = False, suppress_max_turns_warning: bool = False, use_responses_api: bool = False, llm_backend: LLMBackend | str = LLMBackend.LITELLM, llm_kwargs: dict | None = None, llm_call_kwargs: dict[str, Any] | None = None, extra_env: dict[str, str] | None = None, *args, **kwargs)

  • 参数: logs_dir: Path — 传给父类构造函数的日志目录; model_name: str | None — 目标模型名;缺失时直接报错; max_turns: int | None — 新的最大回合/episode 限制入口,优先级最高; parser_name: str — 解析器名称,写入 self._parser_name 并驱动 _get_parser(); api_base: str | None — 传给 _init_llm 的 API 基地址; temperature: float | None — 采样温度;同时写入 self._temperature 并传给 _init_llm; reasoning_effort: Literal[...] | None — 推理力度配置;写入实例并传给 _init_llm; collect_rollout_details: bool — 是否采集 rollout 细节;写入实例并传给 _init_llm; session_id: str | None — 外部提供的会话标识;若为空则生成 UUID; enable_summarize: bool — 是否启用总结能力,写入 self._enable_summarize; proactive_summarization_threshold: int — 主动总结阈值,写入 self._proactive_summarization_threshold; max_thinking_tokens: int | None — 传给 _init_llm 的 thinking token 上限; model_info: dict | None — 可选模型元信息,先经 _resolve_model_info 处理; trajectory_config: TrajectoryConfig | None — 轨迹配置;为空时退化为 {},否则原样保留引用; tmux_pane_width: int — tmux pane 宽度配置; tmux_pane_height: int — tmux pane 高度配置; store_all_messages: bool — 是否保存全部消息到实例配置; record_terminal_session: bool — 是否记录终端会话; interleaved_thinking: bool — 是否启用交错 thinking 历史; suppress_max_turns_warning: bool — 是否抑制显式最大回合限制告警; use_responses_api: bool — 传给 _init_llm 的 API 选择开关; llm_backend: LLMBackend | str — LLM 后端类型,传给 _init_llm; llm_kwargs: dict | None — LLM 构造附加参数;传给 _init_llm 并保存到 self._llm_kwargs; llm_call_kwargs: dict[str, Any] | None — LLM 调用级附加参数;若非空则复制后保存; extra_env: dict[str, str] | None — 额外环境变量映射,保存到 self._extra_env; args: tuple — 透传给父类构造函数的附加位置参数; kwargs: dict — 透传给父类且用于兼容读取 episodes/max_episodes 的附加关键字参数
  • 读状态: self.logger, self._resolve_model_info, self._init_llm, self._get_parser, self._get_prompt_template_path, self._get_timeout_template_path
  • 返回: 返回 None;真正产出是实例配置、默认运行态字段与模板文本的初始化。
  • 副作用: 调用 super().__init__(logs_dir, model_name, *args, **kwargs) 初始化父类状态; 当 model_name is None 时抛出 ValueError('model_name is required for Terminus 2'); 写入 self._extra_envself._model_nameself._last_response_model_nameself._parser_nameself._collect_rollout_detailsself._reasoning_effort; 调用 _resolve_model_info(...) 并把结果传入 _init_llm(...); 写入 self._llmself._parserself._prompt_templateself._timeout_templateself._temperature; 根据 max_turns / kwargs['max_episodes'] / kwargs['episodes'] 计算并写入 self._max_episodes; 在检测到 kwargs['episodes'] 时通过 self.logger.warning(...) 发出废弃参数告警; 在存在显式最大回合限制且未抑制时通过 self.logger.warning(...) 发出限制告警; 写入 self._chatself._contextself._timestamped_markersself._pending_completionself._sessionself._api_request_timesself._n_episodes; 写入 self._user_provided_session_idself._session_id(必要时生成 UUID); 写入 self._trajectory_stepsself._record_terminal_sessionself._llm_call_kwargs; 写入 self._summarization_countself._pending_subagent_refsself._pending_handoff_promptself._subagent_metricsself._subagent_rollout_details; 写入 self._enable_summarizeself._proactive_summarization_thresholdself._tmux_pane_widthself._tmux_pane_height; 写入 self._trajectory_configself._save_raw_content_in_trajectoryself._linear_historyself._store_all_messagesself._interleaved_thinkingself._llm_kwargs

执行流

  1. 先调用父类构造函数 super().__init__(logs_dir, model_name, *args, **kwargs),随后保存 extra_env,并对 model_name 做显式空值校验;若为 None 立即抛出 ValueError,不继续初始化。
  2. 把基础模型相关配置冻结到实例:写入 self._model_nameself._parser_nameself._collect_rollout_detailsself._reasoning_effort,并通过 _resolve_model_info(model_name, model_info) 取得传给 LLM 初始化的模型元信息。
  3. 调用 _init_llm(...) 构造 self._llm,参数中纳入模型名、温度、rollout 开关、api_basesession_idmax_thinking_tokensreasoning_effort、解析后的 model_info 以及 use_responses_api;随后经 _get_parser() 生成 self._parser,并从 _get_prompt_template_path()_get_timeout_template_path() 读取模板文本到 self._prompt_templateself._timeout_template
  4. 处理兼容参数:先读取 kwargs.get('episodes'),若存在则单独发出“已废弃,请改用 max_turns”的告警;然后按 max_turns > kwargs['max_episodes'] > kwargs['episodes'] 的顺序求出 final_max_episodes
  5. 若求得显式上限,则在 suppress_max_turns_warning 为假时发出“人为限制 max_turns/max_episodes”的告警,并写入 self._max_episodes;若未提供任何上限,则把 self._max_episodes 设为 1000000
  6. 声明运行态默认值:把聊天/上下文/tmux/计数器/轨迹/请求计时等字段初始化为空或零值,包括 self._chat = Noneself._context = Noneself._timestamped_markers = []self._pending_completion = Falseself._session = Noneself._api_request_times = []self._n_episodes = 0self._trajectory_steps = []
  7. 完成会话与总结相关配置冻结:保留用户传入的 session_idself._user_provided_session_id,同时把 self._session_id 设为传入值或新生成 UUID;并初始化 self._summarization_count = 0self._pending_subagent_refs = Noneself._pending_handoff_prompt = Noneself._subagent_metrics = SubagentMetrics()self._subagent_rollout_details = [],再写入总结阈值与 tmux 尺寸。
  8. 最后固化轨迹与调用级配置:self._trajectory_config = trajectory_config or {},从中读取 raw_contentlinear_history 标志;把 llm_call_kwargs 在非空时复制为新字典后存入 self._llm_call_kwargs,并写入 self._store_all_messagesself._interleaved_thinkingself._llm_kwargs

源码

    def __init__(
        self,
        logs_dir: Path,
        model_name: str | None = None,
        max_turns: int | None = None,
        parser_name: str = "json",
        api_base: str | None = None,
        temperature: float | None = None,
        reasoning_effort: Literal[
            "none", "minimal", "low", "medium", "high", "xhigh", "max", "default"
        ]
        | None = None,
        collect_rollout_details: bool = False,
        session_id: str | None = None,
        enable_summarize: bool = True,
        proactive_summarization_threshold: int = 8000,
        max_thinking_tokens: int | None = None,
        model_info: dict | None = None,
        trajectory_config: TrajectoryConfig | None = None,
        tmux_pane_width: int = 160,
        tmux_pane_height: int = 40,
        store_all_messages: bool = False,
        record_terminal_session: bool = True,
        interleaved_thinking: bool = False,
        suppress_max_turns_warning: bool = False,
        use_responses_api: bool = False,
        llm_backend: LLMBackend | str = LLMBackend.LITELLM,
        llm_kwargs: dict | None = None,
        llm_call_kwargs: dict[str, Any] | None = None,
        extra_env: dict[str, str] | None = None,
        *args,
        **kwargs,
    ):
        """Initialize Terminus 2 agent.

        Args:
            logs_dir: Directory to store logs
            model_name: Name of the model to use
            max_episodes: Maximum number of episodes (default: 1000000)
            parser_name: Parser to use - "json" or "xml" (default: "json")
            api_base: Base URL for the API endpoint
            temperature: Optional sampling temperature. If unset, no temperature is
                passed to the LLM backend. (default: None)
            reasoning_effort: Qualitative or quantitative measure of effort (default: None)
            collect_rollout_details: Whether to collect detailed rollout data including token IDs.
                NOTE: Rollout details will be incomplete if context summarization occurs.
                See class docstring for details. (default: False)
            session_id: Session ID for the agent (default: None)
            enable_summarize: Whether to enable context summarization (default: True)
            proactive_summarization_threshold: Number of free tokens below which to trigger
                proactive summarization. Set to 0 to disable proactive summarization. (default: 8000)
            max_thinking_tokens: Maximum thinking tokens for Anthropic extended thinking mode.
                Minimum value is 1024. (default: None)
            model_info: Optional dict containing model information for custom models.
                Used to register the model with litellm. Common fields include:
                - max_input_tokens: Maximum input tokens (context length)
                - max_output_tokens: Maximum output tokens
                - input_cost_per_token: Cost per input token (optional)
                - output_cost_per_token: Cost per output token (optional)
                (default: None)
            trajectory_config: Optional TrajectoryConfig containing trajectory-related configurations.
                Available options:
                - raw_content (bool): If True, dump raw LLM responses into trajectory without
                  parsing into tool_calls. Useful for SFT data export. (default: False)
                - linear_history (bool): If True, split trajectory into separate files when context
                  summarization occurs, ensuring each trajectory represents a continuous linear
                  history sent to the LLM. When False, keep all steps from the main agent
                  in a single trajectory file despite chat history resets. (default: False)
                (default: None)
            tmux_pane_width: Starting tmux pane width (maps to `tmux -x`, default: 160)
            tmux_pane_height: Starting tmux pane height (maps to `tmux -y`, default: 40)
            record_terminal_session: Whether to capture terminal recordings via asciinema.
                (default: True)
            interleaved_thinking: Whether to include reasoning content in chat history
                and send to litellm in next round's conversation (default: False)
            suppress_max_turns_warning: Whether to suppress the warning about artificially
                limiting max_turns (default: False)
            llm_backend: LLM backend to use. Use LLMBackend.LITELLM or "litellm".
                (default: LLMBackend.LITELLM)
            llm_kwargs: Additional kwargs to pass to the LLM constructor.
                (default: None)
            llm_call_kwargs: Extra kwargs to forward to LLM calls (e.g., extra_body).
            **kwargs: Additional arguments
        """
        super().__init__(logs_dir, model_name, *args, **kwargs)
        self._extra_env = extra_env

        if model_name is None:
            raise ValueError("model_name is required for Terminus 2")

        self._model_name = model_name
        self._last_response_model_name: str | None = None
        self._parser_name = parser_name
        self._collect_rollout_details = collect_rollout_details
        self._reasoning_effort = reasoning_effort
        resolved_model_info = self._resolve_model_info(model_name, model_info)

        self._llm = self._init_llm(
            llm_backend=llm_backend,
            model_name=model_name,
            temperature=temperature,
            collect_rollout_details=collect_rollout_details,
            llm_kwargs=llm_kwargs,
            api_base=api_base,
            session_id=session_id,
            max_thinking_tokens=max_thinking_tokens,
            reasoning_effort=reasoning_effort,
            model_info=resolved_model_info,
            use_responses_api=use_responses_api,
        )
        self._parser = self._get_parser()
        self._prompt_template = self._get_prompt_template_path().read_text()
        self._timeout_template = self._get_timeout_template_path().read_text()
        self._temperature = temperature

        # Handle deprecated 'episodes' kwarg
        episodes_from_kwargs = kwargs.get("episodes")
        if episodes_from_kwargs is not None:
            self.logger.warning(
                "The 'episodes' parameter is deprecated and will be removed in a future version. "
                "Please use 'max_turns' instead."
            )

        # Determine the final max episodes value with proper precedence:
        # 1. max_turns (new parameter)
        # 2. max_episodes (deprecated but still supported)
        # 3. episodes from kwargs (deprecated)
        # 4. Default value of 1000000
        final_max_episodes = None
        if max_turns is not None:
            final_max_episodes = max_turns
        elif kwargs.get("max_episodes") is not None:
            final_max_episodes = kwargs.get("max_episodes")
        elif episodes_from_kwargs is not None:
            final_max_episodes = episodes_from_kwargs

        if final_max_episodes is not None:
            if not suppress_max_turns_warning:
                self.logger.warning(
                    f"max_turns (f.k.a. max_episodes) artificially limited to {final_max_episodes}. "
                    "Consider removing this limit for better task completion."
                )
            self._max_episodes = final_max_episodes
        else:
            self._max_episodes = 1000000
        self._chat: Chat | None = None
        self._context: AgentContext | None = None
        self._timestamped_markers: list[tuple[float, str]] = []
        self._pending_completion = False
        self._session: TmuxSession | None = None
        self._api_request_times: list[float] = []
        self._n_episodes: int = 0
        self._user_provided_session_id: str | None = session_id
        self._session_id = session_id if session_id else str(uuid.uuid4())
        self._trajectory_steps: list[Step] = []
        self._record_terminal_session = record_terminal_session
        self._llm_call_kwargs = dict(llm_call_kwargs) if llm_call_kwargs else {}

        self._summarization_count: int = (
            0  # Track number of summarization subagents created
        )
        self._pending_subagent_refs: list[SubagentTrajectoryRef] | None = (
            None  # Track subagent refs to include in next step
        )
        self._pending_handoff_prompt: str | None = (
            None  # Track handoff prompt to include as user step
        )
        self._subagent_metrics = SubagentMetrics()  # Track subagent metrics separately
        self._subagent_rollout_details: list[
            RolloutDetail
        ] = []  # Track rollout details for each subagent
        self._enable_summarize = (
            enable_summarize  # Toggle for proactive and context limit summarization
        )
        self._proactive_summarization_threshold = proactive_summarization_threshold
        self._tmux_pane_width = tmux_pane_width
        self._tmux_pane_height = tmux_pane_height

        # Trajectory configuration
        self._trajectory_config = trajectory_config or {}
        self._save_raw_content_in_trajectory = self._trajectory_config.get(
            "raw_content", False
        )
        self._linear_history = self._trajectory_config.get("linear_history", False)
        # Optional: include full chat messages in TrialResult metadata (can be large)
        self._store_all_messages = store_all_messages
        self._interleaved_thinking = interleaved_thinking
        self._llm_kwargs = llm_kwargs

Non-obvious 设计决策

  • model_name 使用显式失败策略而非延后容错:代码在完成父类初始化后立刻检查 if model_name is None: raise ValueError(...)。这样避免后续 _resolve_model_info_init_llm 等路径在缺关键标识时产生更晚且更模糊的错误。
  • 兼容参数采用双层处理而不是简单别名替换:kwargs['episodes'] 仅用于兼容读取,并单独触发废弃告警;真正的上限解析再与 max_turnskwargs['max_episodes'] 一起进入优先级判定。这把“接口迁移提醒”和“人为限制回合数提醒”拆成了两个独立分支,避免一条告警混杂两种含义。
  • 最大回合数的优先级是显式新参数优先:max_turns 覆盖旧的 max_episodes 与更旧的 episodes,未提供时回退到 1000000。这体现出向新接口迁移同时保留旧调用兼容性的取舍;若不做优先级分层,旧参数可能悄然覆盖新参数。
  • 会话标识保留“双轨”信息:self._user_provided_session_id 保存外部输入原值,而 self._session_id 保证始终有值(必要时生成 UUID)。这样实例内部总有可用会话 ID,同时不丢失“是否由用户显式提供”的事实。
  • 配置快照策略并不一致,且是代码中的真实取舍:llm_call_kwargs 在保存前用 dict(...) 复制,避免外部后续修改影响实例;但 trajectory_config 仅做 trajectory_config or {},当传入真值对象时并未复制,而是保留原引用。也就是说,前者被快照隔离,后者没有完全隔离。

上下游关系

  • 调用方: 直接实例化 Terminus2(...) 的外部调用点; 任何子类若未覆盖构造函数而沿用本构造逻辑; 测试代码中创建 Terminus2 实例的入口; 框架/脚本层通过构造器注入配置时的对象创建路径
  • 核心被调用: Base/父类 __init__(经 super().__init__(...)); self._resolve_model_info(model_name, model_info); self._init_llm(...); self._get_parser(); self._get_prompt_template_path().read_text(); self._get_timeout_template_path().read_text(); uuid.uuid4(); SubagentMetrics(); self.logger.warning(...)
  • 配置/状态来源: 构造参数 model_nameparser_nametemperaturereasoning_effortapi_base; 构造参数 enable_summarizeproactive_summarization_thresholdtrajectory_config; 构造参数 tmux_pane_widthtmux_pane_heightrecord_terminal_sessionstore_all_messagesinterleaved_thinking; 构造参数 llm_backendllm_kwargsllm_call_kwargsuse_responses_apimax_thinking_tokens; kwargs.get('episodes')kwargs.get('max_episodes') 的兼容输入; _get_prompt_template_path()_get_timeout_template_path() 指向的模板文件文本
  • 结果去向: 实例字段 self._llmself._parserself._prompt_templateself._timeout_template; 实例字段 self._max_episodesself._temperatureself._llm_call_kwargsself._llm_kwargs; 实例字段 self._session_idself._user_provided_session_idself._extra_env; 实例字段 self._trajectory_configself._save_raw_content_in_trajectoryself._linear_history; 实例字段 self._chatself._contextself._trajectory_stepsself._api_request_times; 实例字段 self._pending_completionself._pending_subagent_refsself._pending_handoff_promptself._summarization_count
寄存器交互
Terminus2._resolve_model_infoterminus_2.py:333–345 ↗

模型元信息选择与告警入口

stage 上下文: 该函数是一个很小的配置裁决点:在两个输入源之间决定返回哪份模型元信息,或明确返回 None。与同阶段里负责固化大量实例字段的兄弟函数相比,它本身不写入实例状态,只提供一个可被上层采用的返回值。其特殊性仅体现在对 model_name"hosted_vllm" 模式的告警处理。

这段代码在干什么

函数 _resolve_model_info 接收 model_nameprovided_model_info,优先返回真值的 provided_model_info。若 provided_model_info 为假值,则在 model_name 为真值且包含子串 "hosted_vllm" 时,通过 self.logger.warning(...) 发出一条配置告警。除此之外它不修改任何实例字段,最终返回值要么是传入的那份字典,要么是 None`;空字典等假值不会被直接返回,而会继续落入后续分支。

接口 · 参数 / IO

(self, model_name: str | None, provided_model_info: dict | None) -> dict | None

  • 参数: model_name: str | None — 候选模型名称;仅在其为真值且包含 hosted_vllm 时参与告警判断; provided_model_info: dict | None — 调用方传入的模型元信息;只有在其为真值时才被原样返回
  • 读状态: self.logger
  • 返回: 返回 dict | None:若 provided_model_info 为真值则返回该对象;否则返回 None
  • 副作用: 当 model_name 为真值且包含 hosted_vllm,并且 provided_model_info 为假值时,调用 self.logger.warning(...) 输出一条告警日志

执行流

  1. 先检查 provided_model_info 的真值性;若条件 if provided_model_info: 成立,则立即返回该参数本身,不再执行后续逻辑。
  2. 若前一步未返回,再检查 model_name 的真值性以及表达式 "hosted_vllm" in model_name 是否成立;只有两者同时满足时,才调用 self.logger.warning(...) 记录告警。
  3. 无论是否发出告警,只要没有走到首个提前返回分支,函数最后都执行 return None

源码

    def _resolve_model_info(
        self, model_name: str | None, provided_model_info: dict | None
    ) -> dict | None:
        if provided_model_info:
            return provided_model_info
        if model_name and "hosted_vllm" in model_name:
            self.logger.warning(
                "Model info is required when using hosted_vllm models. "
                "Please set `model_info` in your Terminus 2 configuration with "
                "`max_input_tokens`, `max_output_tokens`, and cost fields. "
                "Falling back to LiteLLM defaults, which may cause context or pricing issues."
            )
        return None

Non-obvious 设计决策

  • 函数把“接受调用方给出的模型信息”写成 if provided_model_info:,这意味着只有真值字典才会被采用;空字典 {} 虽然类型上仍是 dict,但会被当作未命中而继续走后续判断。这是一个不那么显眼但非常具体的取舍。
  • hosted_vllm 的处理选择了“告警但不阻断”:代码只记录 warning,没有抛异常、没有补写默认值、也没有在本函数内执行任何回退逻辑。关于“Falling back to LiteLLM defaults, which may cause context or pricing issues.” 的表述仅出现在告警消息文本中,是提示语义,而不是该函数额外实现的行为。
  • 特殊分支采用简单的子串匹配 "hosted_vllm" in model_name,而不是更严格的结构化解析;因此它只依据名称模式触发提醒,保持函数职责局限在轻量判定与提示。

上下游关系

  • 调用方: 上层配置装配代码,向其请求本次应采用的模型元信息; 任何持有 selfmodel_nameprovided_model_info 的实例方法
  • 核心被调用: self.logger.warning
  • 配置/状态来源: 参数 model_name; 参数 provided_model_info; 实例上的 self.logger
  • 结果去向: 返回给调用方的一份 dict 模型元信息; 返回给调用方的 None; 日志系统中的一条 warning 记录
  • 同类 sibling: Terminus2.__init__:兄弟函数负责把大量配置写入实例字段;本函数只做一处模型元信息选择并可能发出告警。
Terminus2._init_llmterminus_2.py:73–142 ↗

LLM 后端实例化分发点

stage 上下文: 该函数处于 stage-1 的早段,负责把 Terminus2.__init__ 已接收但尚未固化的 LLM 相关配置,收束为一个可直接使用的聊天后端对象。触发条件是构造器初始化期间需要建立 self._llm。与同 stage 的 _resolve_model_info 相比,它不再判断模型元信息来源,而是消费这些已解析配置并完成后端分派。

这段代码在干什么

函数 _init_llm 根据 llm_backend 选择具体的 LLM 客户端实现,并返回一个 BaseLLM 兼容实例。它只读取传入参数:后端选择、模型名、采样温度、rollout 细节开关、通用 llm_kwargs,以及 LiteLLM 专属的 api_basesession_idmax_thinking_tokensreasoning_effortmodel_infouse_responses_api。函数本身不写入任何 self 状态;其产出会被 __init__ 用来赋给 self._llm。若后端值不在支持列表内,则抛出带支持项清单的 ValueError

接口 · 参数 / IO

(self, llm_backend: LLMBackend | str, model_name: str, temperature: float | None, collect_rollout_details: bool, llm_kwargs: dict | None, api_base: str | None, session_id: str | None, max_thinking_tokens: int | None, reasoning_effort: str | None, model_info: dict | None, use_responses_api: bool) -> BaseLLM

  • 参数: llm_backend: LLMBackend | str — 后端选择器;既接受枚举值也接受字符串值,用于决定实例化 LiteLLM 还是 TinkerLLM; model_name: str — 传给具体 LLM 构造器的模型标识; temperature: float | None — 显式配置的采样温度;仅在非 None 时并入构造参数; collect_rollout_details: bool — 是否收集 token id / logprob 等 rollout 细节,向各后端透传; llm_kwargs: dict | None — 调用方提供的通用后端构造参数,会先复制再补充专门字段; api_base: str | None — LiteLLM 分支的 API 基址; session_id: str | None — LiteLLM 分支的会话标识; max_thinking_tokens: int | None — LiteLLM 扩展 thinking 配额控制; reasoning_effort: str | None — LiteLLM 推理强度等级; model_info: dict | None — LiteLLM 自定义模型信息字典;通常来自兄弟函数 _resolve_model_info 的结果; use_responses_api: bool — LiteLLM 是否改走 Responses API
  • 返回: 返回一个已初始化的 BaseLLM 兼容对象:当后端为 litelm 时返回 LiteLLM,当后端为 tinker 时返回 TinkerLLM
  • 副作用: 无 self 状态写入; 在 tinker 分支发生一次延迟导入:from harbor.llms.tinker import TinkerLLM; 非法后端时抛出 ValueError 终止初始化流程

执行流

  1. 先把 llm_backend 规范化为可匹配的字符串 backend_value:若传入的是 LLMBackend 枚举,则取其 .value,否则直接使用原字符串。
  2. 基于 llm_kwargs or {} 构造一份新的 constructor_kwargs,避免直接复用调用方传入字典;若 temperature is not None,再把 temperature 显式并入该字典。
  3. 进入 match backend_value 分派:当值等于 LLMBackend.LITELLM.value 时,实例化并返回 LiteLLM,同时传入 LiteLLM 专属参数 api_basesession_idmax_thinking_tokensreasoning_effortmodel_infouse_responses_api,以及通用的 model_namecollect_rollout_details 和展开后的 constructor_kwargs
  4. 当值等于 LLMBackend.TINKER.value 时,先在分支内延迟导入 TinkerLLM,再用 model_namecollect_rollout_detailsconstructor_kwargs 实例化并返回。
  5. backend_value 不匹配任何已知分支,则抛出 ValueError;错误消息同时包含原始 llm_backend 表示值和 [b.value for b in LLMBackend] 生成的支持后端列表。

源码

    def _init_llm(
        self,
        llm_backend: LLMBackend | str,
        model_name: str,
        temperature: float | None,
        collect_rollout_details: bool,
        llm_kwargs: dict | None,
        # LiteLLM-specific args
        api_base: str | None,
        session_id: str | None,
        max_thinking_tokens: int | None,
        reasoning_effort: str | None,
        model_info: dict | None,
        use_responses_api: bool,
    ) -> BaseLLM:
        """Initialize the LLM backend based on llm_backend parameter.

        Args:
            llm_backend: The LLM backend to use.
            model_name: Name of the model.
            temperature: Sampling temperature, if explicitly configured.
            collect_rollout_details: Whether to collect token IDs and logprobs.
            llm_kwargs: Additional kwargs passed to the LLM constructor.
            api_base: Base URL for LiteLLM API endpoint.
            session_id: Session ID for LiteLLM.
            max_thinking_tokens: Max thinking tokens for LiteLLM extended thinking.
            reasoning_effort: Reasoning effort level for LiteLLM.
            model_info: Model info dict for LiteLLM custom models.
            use_responses_api: Whether to use the Responses API.

        Returns:
            An initialized LLM instance.

        Raises:
            ValueError: If llm_backend is not a recognized backend.
        """
        # Normalize enum to string value for matching
        backend_value = (
            llm_backend.value if isinstance(llm_backend, LLMBackend) else llm_backend
        )
        constructor_kwargs = dict(llm_kwargs or {})
        if temperature is not None:
            constructor_kwargs["temperature"] = temperature

        match backend_value:
            case LLMBackend.LITELLM.value:
                return LiteLLM(
                    model_name=model_name,
                    api_base=api_base,
                    collect_rollout_details=collect_rollout_details,
                    session_id=session_id,
                    max_thinking_tokens=max_thinking_tokens,
                    reasoning_effort=reasoning_effort,
                    model_info=model_info,
                    use_responses_api=use_responses_api,
                    **constructor_kwargs,
                )
            case LLMBackend.TINKER.value:
                from harbor.llms.tinker import TinkerLLM

                return TinkerLLM(
                    model_name=model_name,
                    collect_rollout_details=collect_rollout_details,
                    **constructor_kwargs,
                )
            case _:
                raise ValueError(
                    f"Unknown llm_backend: {llm_backend!r}. "
                    f"Supported backends: {[b.value for b in LLMBackend]}"
                )

Non-obvious 设计决策

  • 该函数同时接受 LLMBackend 枚举和裸字符串,并在开头统一折叠到 backend_value。这样调用方可以保留更宽松的配置入口,而后续匹配逻辑只需处理一种比较形式;若不先规范化,match 分支就必须同时覆盖枚举对象与字符串两套形态。
  • constructor_kwargs = dict(llm_kwargs or {}) 体现的是“复制后增强”而非“原地修改”。这避免了把 temperature 等补充参数回写到调用方持有的原始字典里,使 _init_llm 保持纯构造语义。
  • temperature 使用显式 is not None 检查,而不是泛真值判断。这保证了像 0 这样的合法配置不会被误判为缺省值,从而能够被真实传入后端构造器。
  • TinkerLLM 放在对应分支里延迟导入,说明该依赖只在选择 tinker 后端时才需要解析。这样可以减少无关后端的导入负担,也避免在未使用 tinker 时因为缺少相关依赖而过早失败。
  • 未知后端不是静默回退,而是立即抛出附带支持项列表的 ValueError。这一取舍把配置错误暴露在初始化阶段,避免实例进入半初始化状态;同时错误信息直接给出合法候选值,降低排查成本。

上下游关系

  • 调用方: Terminus2.__init__:在实例初始化时调用本函数并把结果写入 self._llm
  • 核心被调用: LiteLLM(...); harbor.llms.tinker.TinkerLLM(...); LLMBackend 枚举值比较; ValueError(...)
  • 配置/状态来源: 来自 Terminus2.__init__ 形参的 llm_backend 配置; 来自 Terminus2.__init__ 形参的 model_nametemperature; 来自 Terminus2.__init__ 形参的 collect_rollout_detailsllm_kwargs; 来自 _resolve_model_info 产出的 model_info,再经 __init__ 传入本函数; 来自 Terminus2.__init__ 形参的 LiteLLM 专属设置:api_basesession_idmax_thinking_tokensreasoning_effortuse_responses_api
  • 结果去向: Terminus2.__init__self._llm 字段初始化; 后续 stage 中所有经 self._llm 发起的模型请求路径; 与 _get_parser 并列,作为 agent 运行时依赖装配的一部分
  • 同类 sibling: Terminus2.__init__:负责汇总用户配置并消费本函数返回值; Terminus2._resolve_model_info:先决定 model_info,再把结果提供给本函数中的 LiteLLM 分支
Terminus2._get_parserterminus_2.py:375–384 ↗

按解析器名称分派解析实例

stage 上下文: 该单元位于配置固化阶段中,职责很窄:把实例上已有的 self._parser_name 解释为一个具体解析器对象。触发条件从函数体本身只能确认是“当外部代码调用 _get_parser() 时”。与同 stage 的 _resolve_model_info_init_llm 类似,它本身不写入实例状态,而是返回一个供外部继续使用的组件对象。

这段代码在干什么

函数 _get_parser 读取 self._parser_name,仅支持两种取值:"json""xml"。当值分别匹配时,它返回一个新建的 TerminusJSONPlainParser()TerminusXMLPlainParser() 实例;除此之外不产生任何状态写入。若配置值不在这两个分支内,则立即抛出 ValueError,并在错误信息中直接列出允许值。

接口 · 参数 / IO

(self)

  • 参数: self: Terminus2 — 提供实例配置状态,函数仅从中读取 self._parser_name
  • 读状态: self._parser_name
  • 返回: 返回一个新建的解析器实例:当 self._parser_name == "json" 时返回 TerminusJSONPlainParser();当 self._parser_name == "xml" 时返回 TerminusXMLPlainParser();若两者都不是,则不返回而是抛出 ValueError("Unknown parser_name: {self._parser_name}. Use 'json' or 'xml'.")

执行流

  1. 先读取实例字段 self._parser_name,判断其是否等于字符串 "json"
  2. 若为 "json",立即构造并返回一个新的 TerminusJSONPlainParser() 实例。
  3. 否则继续判断 self._parser_name 是否等于字符串 "xml"
  4. 若为 "xml",立即构造并返回一个新的 TerminusXMLPlainParser() 实例。
  5. 若前述两个分支都不匹配,则进入 else,抛出 ValueError;错误文本会插入当前的 self._parser_name,并明确提示只允许 'json''xml'

源码

    def _get_parser(self):
        """Return the appropriate parser instance for this format."""
        if self._parser_name == "json":
            return TerminusJSONPlainParser()
        elif self._parser_name == "xml":
            return TerminusXMLPlainParser()
        else:
            raise ValueError(
                f"Unknown parser_name: {self._parser_name}. Use 'json' or 'xml'."
            )

Non-obvious 设计决策

  • 代码采用封闭式二选一分派,而不是默认回退到某个解析器;这样未识别配置会被 ValueError 立刻拒绝,不会静默降级。
  • 异常消息不是泛泛报错,而是把实际收到的 self._parser_name 与允许值 'json''xml' 一并写出,便于直接定位配置错误。
  • 两个成功分支都通过直接调用类构造器返回新对象,说明该函数选择“每次调用新建实例”,而不是复用共享单例。

上下游关系

  • 调用方: 未知外部调用方:源码片段本身未显示谁调用 _get_parser
  • 核心被调用: TerminusJSONPlainParser(); TerminusXMLPlainParser(); ValueError(...)
  • 配置/状态来源: self._parser_name
  • 结果去向: 返回给 _get_parser 的直接调用者; 异常路径返回给调用栈上的错误处理方
  • 同类 sibling: Terminus2._resolve_model_info:同样是读取输入/状态后返回结果,本体不写实例字段; Terminus2._init_llm:同样承担组件选择/构造职责,但 _get_parser 的分派集合更小且只依赖 self._parser_name
Terminus2._get_prompt_template_pathterminus_2.py:386–395 ↗

按解析器名选择 prompt 模板路径

stage 上下文: 该单元是一个纯查询式辅助函数:根据当前 self._parser_name 计算模板文件路径。它本身不写入实例状态,只在当前模块目录下的 templates 子目录中二选一返回文件路径;若配置值不受支持,则立即报错。

这段代码在干什么

函数 _get_prompt_template_path 读取 self._parser_name,并基于模块文件位置 __file__ 组装对应 prompt 模板的 Path。当解析器名为 "json" 时返回 terminus-json-plain.txt,为 "xml" 时返回 terminus-xml-plain.txt。除返回值外无其他产出;若取值不是这两者,则抛出 ValueError

接口 · 参数 / IO

(self) -> Path

  • 参数: self: ? — 提供实例配置字段 self._parser_name; __file__: str — 当前模块文件路径;经 Path(__file__).parent 作为模板目录定位基准
  • 读状态: self._parser_name, __file__
  • 返回: 返回一个 pathlib.Path,其共同模式为 Path(__file__).parent / "templates" / <filename>;当 self._parser_name 不为 jsonxml 时改为抛出 ValueError

执行流

  1. 先读取 self._parser_name 并判断是否等于字符串 "json"
  2. 若为 "json",返回 Path(__file__).parent / "templates" / "terminus-json-plain.txt"
  3. 否则继续判断 self._parser_name 是否等于字符串 "xml"
  4. 若为 "xml",返回 Path(__file__).parent / "templates" / "terminus-xml-plain.txt"
  5. 若上述两种取值都不匹配,则抛出 ValueError;异常消息同时包含实际值 self._parser_name 与允许值 "json""xml"

源码

    def _get_prompt_template_path(self) -> Path:
        """Return the path to the prompt template for this format."""
        if self._parser_name == "json":
            return Path(__file__).parent / "templates" / "terminus-json-plain.txt"
        elif self._parser_name == "xml":
            return Path(__file__).parent / "templates" / "terminus-xml-plain.txt"
        else:
            raise ValueError(
                f"Unknown parser_name: {self._parser_name}. Use 'json' or 'xml'."
            )

Non-obvious 设计决策

  • 模板路径采用 Path(__file__).parent 作为基准,而不是依赖外部传入目录;这使查找位置固定为当前模块旁的 templates 子目录。
  • 成功分支只接受两种显式字符串:"json""xml";代码没有默认回退模板,未知取值会直接失败。
  • 异常信息不是泛化报错,而是把非法的 self._parser_name 实际值嵌入消息,并明确给出可用选项 Use 'json' or 'xml'.,便于定位配置错误。

上下游关系

  • 调用方: 未知;给定源码片段未显示调用点
  • 核心被调用: Path(__file__)
  • 配置/状态来源: self._parser_name; __file__
  • 结果去向: 函数调用方接收返回的 Path; 异常路径下由调用方接收 ValueError
Terminus2._get_timeout_template_pathterminus_2.py:397–399 ↗

超时模板路径构造器

stage 上下文: 该函数本体只证明一件事:基于当前源码文件位置构造一个固定模板路径并返回。它不读取任何 self 实例状态,也不执行文件存在性检查或内容加载。就可见代码而言,它是一个纯路径定位辅助函数。

这段代码在干什么

函数 _get_timeout_template_path 接收 self,但函数体并未使用任何实例字段。它直接返回表达式 Path(__file__).parent / "templates" / "timeout.txt" 生成的 Path 对象。除该返回值外无其他产出,也不会修改对象状态。

接口 · 参数 / IO

(self: Terminus2) -> Path

  • 参数: self: Terminus2 — 方法接收者;在此函数体内未被读取
  • 返回: 返回精确由 Path(__file__).parent / "templates" / "timeout.txt" 构造出的 Path;不读取文件、不验证路径是否存在。

执行流

  1. 调用 Path(__file__) 取得当前模块源码文件对应的路径对象。
  2. 取其 .parent 作为基目录,并依次拼接子路径 "templates""timeout.txt"
  3. 返回得到的 Path 对象作为超时模板文件路径。

源码

    def _get_timeout_template_path(self) -> Path:
        """Return the path to the timeout template for this format."""
        return Path(__file__).parent / "templates" / "timeout.txt"

Non-obvious 设计决策

  • 路径锚定在 __file__ 所在目录,而不是当前工作目录或实例配置;这一选择使返回值仅由模块物理位置决定。
  • 函数只负责构造路径,不在此处检查文件是否存在、是否可读,因而职责被限定为定位而非验证。

上下游关系

  • 调用方: 未在该函数体内体现;外部任意需要超时模板路径的调用点
  • 核心被调用: pathlib.Path; Path.parent; Path.__truediv__
  • 配置/状态来源: __file__ 模块级文件路径常量
  • 结果去向: 返回给调用方作为 templates/timeout.txt 的路径值