docs: generalize llm-engine comments

This commit is contained in:
Keisuke Hirata 2026-07-14 04:14:58 +09:00
parent 34856b9900
commit 1505c5e8a3
No known key found for this signature in database
16 changed files with 93 additions and 111 deletions

View File

@ -20,7 +20,7 @@ Does not own:
## Design notes ## Design notes
Macros reduce boilerplate, but they must not imply capability. A generated tool definition is still subject to manifest permissions, Worker scope, and runtime policy. Macros reduce boilerplate, but they must not imply capability. A generated tool definition is still subject to host permissions, application scope, and runtime policy.
## See also ## See also

View File

@ -16,10 +16,10 @@ Owns:
Does not own: Does not own:
- Worker names, sockets, process lifecycle, or scope delegation (`worker`) - Host application names, sockets, process lifecycle, or scope delegation
- product CLI shape (`yoi`) - Product CLI shape
- provider catalog and secret resolution (`provider`, `secrets`) - Provider catalog and secret resolution
- durable Worker current state (`session-store` worker metadata) - Durable application state outside engine history
## Design notes ## Design notes

View File

@ -213,9 +213,9 @@ pub struct Engine<C: LlmClient, S: EngineState = Mutable> {
/// stream events become visible. /// stream events become visible.
lifecycle_trace_cbs: Vec<Arc<dyn Fn(usize, usize, &str, &Value) + Send + Sync>>, lifecycle_trace_cbs: Vec<Arc<dyn Fn(usize, usize, &str, &Value) + Send + Sync>>,
/// Non-fatal warning callbacks. Invoked when the Engine wants to /// Non-fatal warning callbacks. Invoked when the Engine wants to
/// surface an advisory message to the upper layer (e.g. Worker) so it /// surface an advisory message to the caller so it can be forwarded
/// can be forwarded to the user — distinct from `tracing::warn!`, /// to the user — distinct from `tracing::warn!`, which is for
/// which is for developer-facing logs. /// developer-facing logs.
warning_cbs: Vec<Box<dyn Fn(&str) + Send + Sync>>, warning_cbs: Vec<Box<dyn Fn(&str) + Send + Sync>>,
/// Tool-result callbacks. Invoked once per completed tool call /// Tool-result callbacks. Invoked once per completed tool call
/// after post-execution interceptors and the output byte-cap /// after post-execution interceptors and the output byte-cap
@ -253,8 +253,8 @@ pub struct Engine<C: LlmClient, S: EngineState = Mutable> {
/// Plumbed into [`Request::cache_anchor`] at request build time. /// Plumbed into [`Request::cache_anchor`] at request build time.
cache_anchor: Option<usize>, cache_anchor: Option<usize>,
/// Conversation-scoped cache key, set by higher layers. Plumbed into /// Conversation-scoped cache key, set by higher layers. Plumbed into
/// [`Request::cache_key`] at request build time. Worker 側では /// [`Request::cache_key`] at request build time. Callers should pass a
/// `SegmentId` を渡す。 /// stable conversation identifier when the backend benefits from one.
cache_key: Option<String>, cache_key: Option<String>,
/// State marker /// State marker
_state: PhantomData<S>, _state: PhantomData<S>,
@ -486,8 +486,8 @@ impl<C: LlmClient, S: EngineState> Engine<C, S> {
/// ///
/// Fired after `post_tool_call` interceptors and any `content` /// Fired after `post_tool_call` interceptors and any `content`
/// truncation from `tool_output_limits`, so the callback observes /// truncation from `tool_output_limits`, so the callback observes
/// exactly what is persisted to history. Intended for upper layers /// exactly what is persisted to history. Intended for callers that need
/// (e.g. Worker) to forward tool results to clients. /// to forward tool results to clients.
pub fn on_tool_result(&mut self, callback: impl Fn(&ToolResult) + Send + Sync + 'static) { pub fn on_tool_result(&mut self, callback: impl Fn(&ToolResult) + Send + Sync + 'static) {
self.tool_result_cbs.push(Box::new(callback)); self.tool_result_cbs.push(Box::new(callback));
} }
@ -1121,10 +1121,10 @@ impl<C: LlmClient, S: EngineState> Engine<C, S> {
} }
// Drain interceptor-side inputs that are meant to land in // Drain interceptor-side inputs that are meant to land in
// history (notifications, cross-Worker events, system // history (notifications, external events, system reminders).
// reminders). These are committed *before* the per-request // These are committed *before* the per-request clone so they
// clone so they participate in the LLM request below and // participate in the LLM request below and get persisted by
// get persisted by the upper layer that owns history.json. // the caller that owns durable history.
let pending = self.interceptor.pending_history_appends().await; let pending = self.interceptor.pending_history_appends().await;
if !pending.is_empty() { if !pending.is_empty() {
self.append_history_items(pending); self.append_history_items(pending);
@ -1301,9 +1301,7 @@ impl<C: LlmClient, S: EngineState> Engine<C, S> {
self.turn_count += 1; self.turn_count += 1;
// Collect and commit assistant items. Routed through // Collect and commit assistant items. Routed through
// `append_history_items` so observers (e.g. the // `append_history_items` so observers see each item as it lands.
// Worker-side per-item session-log committer) see each item
// as it lands.
let reasoning_items = self.thinking_block_collector.take_collected(); let reasoning_items = self.thinking_block_collector.take_collected();
let text_blocks = self.text_block_collector.take_collected(); let text_blocks = self.text_block_collector.take_collected();
let tool_calls = self.tool_call_collector.take_collected(); let tool_calls = self.tool_call_collector.take_collected();
@ -1603,8 +1601,7 @@ impl<C: LlmClient, S: EngineState> Engine<C, S> {
} }
Ok(ToolExecutionResult::Completed(results)) => { Ok(ToolExecutionResult::Completed(results)) => {
// Route per-result pushes through the callback path so // Route per-result pushes through the callback path so
// observers (e.g. the Worker-side per-item session-log // observers see each tool result as it lands.
// committer) see each tool result as it lands.
let items = results.into_iter().map(|result| { let items = results.into_iter().map(|result| {
Item::tool_result_item( Item::tool_result_item(
&result.tool_use_id, &result.tool_use_id,
@ -1707,9 +1704,9 @@ impl<C: LlmClient> Engine<C, Mutable> {
/// Install byte-size caps for tool execution `content`. /// Install byte-size caps for tool execution `content`.
/// ///
/// Passing `None` (the default) disables truncation. Higher layers /// Passing `None` (the default) disables truncation. Callers translate
/// (e.g. Worker) translate manifest configuration into a concrete /// their own configuration into a concrete [`ToolOutputLimits`] and
/// [`ToolOutputLimits`] and install it here. /// install it here.
pub fn set_tool_output_limits(&mut self, limits: Option<ToolOutputLimits>) { pub fn set_tool_output_limits(&mut self, limits: Option<ToolOutputLimits>) {
self.tool_output_limits = limits; self.tool_output_limits = limits;
} }

View File

@ -1,9 +1,8 @@
//! Interceptor - control flow delegation for the Engine execution loop //! Interceptor - control flow delegation for the Engine execution loop
//! //!
//! Defines the [`Interceptor`] trait that upper layers (e.g. Worker) implement //! Defines the [`Interceptor`] trait that callers implement to inject
//! to inject orchestration decisions (approval, skip, pause, abort) //! orchestration decisions (approval, skip, pause, abort) into the Engine's
//! into the Engine's turn loop without the Engine knowing about //! turn loop without the Engine knowing about host-application concepts.
//! higher-level concepts.
use std::sync::Arc; use std::sync::Arc;
@ -132,8 +131,8 @@ pub struct ToolResultInfo {
/// Intercepts the Engine execution loop at key decision points. /// Intercepts the Engine execution loop at key decision points.
/// ///
/// All methods have default implementations that let the Engine /// All methods have default implementations that let the Engine
/// proceed without intervention. Upper layers (e.g. Worker) provide /// proceed without intervention. Callers provide richer implementations for
/// richer implementations for approval flows, permission checks, etc. /// approval flows, permission checks, etc.
#[async_trait] #[async_trait]
pub trait Interceptor: Send + Sync { pub trait Interceptor: Send + Sync {
/// Called after receiving user input, before adding to history. /// Called after receiving user input, before adding to history.
@ -149,7 +148,7 @@ pub trait Interceptor: Send + Sync {
/// ///
/// Use this for inputs that arrive from outside the LLM and need /// Use this for inputs that arrive from outside the LLM and need
/// to be reflected in the on-disk history — notifications, /// to be reflected in the on-disk history — notifications,
/// cross-Worker events, system reminders. Do **not** use /// external events, system reminders. Do **not** use
/// [`Self::pre_llm_request`] for that purpose: it mutates a /// [`Self::pre_llm_request`] for that purpose: it mutates a
/// per-request clone, so any committed assistant response that /// per-request clone, so any committed assistant response that
/// reacts to the injection would have no visible trigger on the /// reacts to the injection would have no visible trigger on the

View File

@ -1,15 +1,11 @@
//! `Scheme` 実装と通信層が要求する認証要件、および動的認証プロバイダ。 //! `Scheme` 実装と通信層が要求する認証要件、および動的認証プロバイダ。
//! //!
//! マニフェスト側の型(`ModelConfig` / `SchemeKind` / `AuthRef`)は
//! `crates/manifest` に置き、llm-engine はそれを知らずに済む。
//! `AuthRequirement` は scheme が宣言する「この scheme はどんな認証を //! `AuthRequirement` は scheme が宣言する「この scheme はどんな認証を
//! 期待するか」のランタイム記述で、manifest 側の `AuthRef` との //! 期待するか」のランタイム記述で、設定ファイルや環境変数などから
//! 照合(`AuthRef → ResolvedAuth` 変換の適否)は `crates/provider` //! [`super::transport::ResolvedAuth`] を組み立てる責務は呼び出し側にある。
//! で行う。
//! //!
//! Codex OAuth のようにリクエスト毎にトークンが変わり得る認証は //! リクエスト毎にトークンが変わり得る認証は [`AuthProvider`] trait を
//! [`AuthProvider`] trait を `crates/provider` 側で実装し、 //! 実装し、[`super::transport::ResolvedAuth::Custom`] 経由で transport に渡す。
//! [`super::transport::ResolvedAuth::Custom`] 経由で transport に渡す。
use async_trait::async_trait; use async_trait::async_trait;
use reqwest::header::{HeaderName, HeaderValue}; use reqwest::header::{HeaderName, HeaderValue};
@ -27,16 +23,15 @@ pub enum AuthRequirement {
XApiKey, XApiKey,
/// クエリパラメータ `?<name>=<token>`Gemini 形式) /// クエリパラメータ `?<name>=<token>`Gemini 形式)
QueryParam { name: &'static str }, QueryParam { name: &'static str },
/// 複合ヘッダ(Codex OAuth 等、`crates/provider` 側で解決) /// 複合ヘッダ(呼び出し側が [`AuthProvider`] で解決)
Custom, Custom,
} }
/// リクエスト毎に認証ヘッダを動的に組み立てるプロバイダ。 /// リクエスト毎に認証ヘッダを動的に組み立てるプロバイダ。
/// ///
/// Codex OAuth のように access_token が refresh で更新されたり、 /// access token が refresh で更新されたり、複数ヘッダを同時に注入する
/// `ChatGPT-Account-Id` / `X-OpenAI-Fedramp` のような複数ヘッダを /// 必要があるケースで使う。実体は呼び出し側に置き、llm-engine は
/// 同時に注入する必要があるケースで使う。実体は `crates/provider` /// trait を知るだけ。
/// 側に置き、llm-engine は trait を知るだけ。
/// ///
/// 返したヘッダはそのまま `HeaderMap` に挿入される。`Authorization` /// 返したヘッダはそのまま `HeaderMap` に挿入される。`Authorization`
/// 含む scheme 既定の認証ヘッダは送出されないので、必要なら /// 含む scheme 既定の認証ヘッダは送出されないので、必要なら
@ -46,11 +41,10 @@ pub trait AuthProvider: Send + Sync + std::fmt::Debug {
/// 1 リクエスト分の認証ヘッダを返す。refresh が必要なら内部で行う。 /// 1 リクエスト分の認証ヘッダを返す。refresh が必要なら内部で行う。
async fn headers(&self) -> Result<Vec<(HeaderName, HeaderValue)>, ClientError>; async fn headers(&self) -> Result<Vec<(HeaderName, HeaderValue)>, ClientError>;
/// ChatGPT Codex backend 向けの複合認証かどうか。 /// Conversation header / request compression が必要な backend profile かどうか。
/// ///
/// transport は provider crate の具象型を知らないため、この hook だけで /// transport は呼び出し側の具象型を知らないため、この hook だけで
/// Codex CLI 互換の wire behaviorconversation header / request compression 等) /// 追加の wire behavior を切り替える。
/// を切り替える。
fn is_codex_backend(&self) -> bool { fn is_codex_backend(&self) -> bool {
false false
} }

View File

@ -45,8 +45,8 @@ pub trait Scheme: Clone + Send + Sync + 'static {
/// プロバイダもあるため、モデル ID を受け取る。 /// プロバイダもあるため、モデル ID を受け取る。
fn path(&self, model_id: &str) -> String; fn path(&self, model_id: &str) -> String;
/// この scheme が要求する認証形式。`build_client` 時に /// この scheme が要求する認証形式。呼び出し側は client 構築時に
/// `manifest::AuthRef` と照合する。 /// 設定された認証情報と照合する。
fn required_auth(&self) -> AuthRequirement; fn required_auth(&self) -> AuthRequirement;
/// `Content-Type` 以外の追加ヘッダ。`anthropic-version` / `anthropic-beta` 等。 /// `Content-Type` 以外の追加ヘッダ。`anthropic-version` / `anthropic-beta` 等。
@ -78,8 +78,7 @@ pub trait Scheme: Clone + Send + Sync + 'static {
/// scheme 既定の capability。モデル ID に関係なく、この wire で /// scheme 既定の capability。モデル ID に関係なく、この wire で
/// 安全に送れる最小共通項を返す。既知モデル ID の能力テーブルは /// 安全に送れる最小共通項を返す。既知モデル ID の能力テーブルは
/// `provider::capability::lookup` 側(高レベル構築層)の責務で、 /// 高レベルの client 構築層の責務で、scheme はここには関与しない。
/// scheme はここには関与しない。
fn default_capability(&self) -> ModelCapability; fn default_capability(&self) -> ModelCapability;
/// scheme 側でサポートしていない `RequestConfig` フィールドを /// scheme 側でサポートしていない `RequestConfig` フィールドを

View File

@ -1,7 +1,7 @@
//! OpenAI Responses scheme の wire-level 既定 capability。 //! OpenAI Responses scheme の wire-level 既定 capability。
//! //!
//! モデル ID 固有のテーブル(`gpt-5` / `codex-` 系など)は高レベル構築層 //! モデル ID 固有の能力テーブルは高レベルの client 構築層の責務。
//! (`provider::capability`)の責務。ここでは wire の保守的 default のみ。 //! ここでは wire の保守的 default のみ。
use crate::llm_client::capability::{ use crate::llm_client::capability::{
CacheStrategy, ModelCapability, StructuredOutput, ToolCallingSupport, CacheStrategy, ModelCapability, StructuredOutput, ToolCallingSupport,

View File

@ -2,7 +2,7 @@
//! //!
//! `response.*` 名前空間の SSE を共通の [`Event`](crate::llm_client::event::Event) //! `response.*` 名前空間の SSE を共通の [`Event`](crate::llm_client::event::Event)
//! に変換する。Responses の (output_index, content_index) 2 次元座標と //! に変換する。Responses の (output_index, content_index) 2 次元座標と
//! yoi 側 1 次元 `BlockStart/Delta/Stop::index` のマッピングは //! この crate の 1 次元 `BlockStart/Delta/Stop::index` のマッピングは
//! [`OpenAIResponsesState`] が保持する。 //! [`OpenAIResponsesState`] が保持する。
use std::collections::{BTreeMap, HashMap}; use std::collections::{BTreeMap, HashMap};

View File

@ -2,7 +2,7 @@
//! //!
//! Chat Completions とは別物の item-based wire format。reasoning item と //! Chat Completions とは別物の item-based wire format。reasoning item と
//! function_call item が first-class で、SSE イベントも `response.*` 名前空間で //! function_call item が first-class で、SSE イベントも `response.*` 名前空間で
//! 流れる。ChatGPT OAuth 経路 (codex) は本 scheme 必須。 //! 流れる。
//! //!
//! - リクエスト JSON 生成: [`request`] //! - リクエスト JSON 生成: [`request`]
//! - SSE イベントパース → [`Event`](crate::llm_client::event::Event) 変換: [`events`] //! - SSE イベントパース → [`Event`](crate::llm_client::event::Event) 変換: [`events`]
@ -19,10 +19,10 @@ pub use scheme_impl::OpenAIResponsesState;
/// `store` / `include_encrypted_content` / `send_max_output_tokens` / /// `store` / `include_encrypted_content` / `send_max_output_tokens` /
/// `send_sampling_params` は scheme 固定の wire 設定で、デフォルトは /// `send_sampling_params` は scheme 固定の wire 設定で、デフォルトは
/// 公式 OpenAI Responses API 向け (stateless + ZDR + `max_output_tokens` /// 公式 OpenAI Responses API 向け (stateless + ZDR + `max_output_tokens`
/// / `temperature` / `top_p` 送出可)。ChatGPT backend (codex-oauth) /// / `temperature` / `top_p` 送出可)。受理パラメータが subset
/// ように受理パラメータが subset の経路では provider 層で /// 互換 backend では client 構築層で `send_max_output_tokens=false` /
/// `send_max_output_tokens=false` / `send_sampling_params=false` に /// `send_sampling_params=false` に上書きする。`ModelCapability` には
/// 上書きする。`ModelCapability` には入れない(モデル能力ではなく wire policy /// 入れない(モデル能力ではなく wire policy
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct OpenAIResponsesScheme { pub struct OpenAIResponsesScheme {
/// サーバ側に response を保存するか。ZDR/stateless 運用では `false`。 /// サーバ側に response を保存するか。ZDR/stateless 運用では `false`。
@ -31,12 +31,12 @@ pub struct OpenAIResponsesScheme {
/// `store=false` で reasoning を使うなら必須。 /// `store=false` で reasoning を使うなら必須。
pub include_encrypted_content: bool, pub include_encrypted_content: bool,
/// `max_output_tokens` を body に載せるか。公式 OpenAI Responses API は /// `max_output_tokens` を body に載せるか。公式 OpenAI Responses API は
/// 受理するが、ChatGPT backend (codex-oauth) は `Unsupported parameter` /// 受理するが、互換 backend によっては `Unsupported parameter` で
/// 400 を返すため、その経路では `false` にする。 /// 400 を返すため、その経路では `false` にする。
pub send_max_output_tokens: bool, pub send_max_output_tokens: bool,
/// `temperature` / `top_p` を body に載せるか。公式 OpenAI Responses API /// `temperature` / `top_p` を body に載せるか。公式 OpenAI Responses API
/// は受理するが、ChatGPT backend (codex-oauth) は `Unsupported parameter` /// は受理するが、互換 backend によっては `Unsupported parameter` で
/// 400 を返すため、その経路では `false` にする。 /// 400 を返すため、その経路では `false` にする。
pub send_sampling_params: bool, pub send_sampling_params: bool,
} }

View File

@ -38,21 +38,21 @@ pub(crate) struct ResponsesRequest {
/// `["reasoning.encrypted_content"]` 等。 /// `["reasoning.encrypted_content"]` 等。
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(skip_serializing_if = "Vec::is_empty")]
pub include: Vec<&'static str>, pub include: Vec<&'static str>,
/// 公式 OpenAI Responses API では受理されるが、ChatGPT backend /// 公式 OpenAI Responses API では受理されるが、互換 backend によっては
/// (codex-oauth) は 400 で弾く。scheme の `send_max_output_tokens` /// 400 で弾く。scheme の `send_max_output_tokens` が `false` のときは
/// が `false` のときは `None` のまま送る (skip_serializing_if で除外)。 /// `None` のまま送る (skip_serializing_if で除外)。
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u32>, pub max_output_tokens: Option<u32>,
/// 公式 OpenAI Responses API では受理されるが、ChatGPT backend /// 公式 OpenAI Responses API では受理されるが、互換 backend によっては
/// (codex-oauth) は `temperature` / `top_p` を 400 で弾く。scheme の /// `temperature` / `top_p` を 400 で弾く。scheme の
/// `send_sampling_params` が `false` のときは `None` のまま送る。 /// `send_sampling_params` が `false` のときは `None` のまま送る。
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>, pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>, pub top_p: Option<f32>,
/// 会話単位の安定キー。ChatGPT backend (codex-oauth) は明示キーが /// 会話単位の安定キー。明示キーを必要とする backend では、
/// 無いとプロンプトキャッシュがほぼ効かない。worker 側は `SegmentId` /// 呼び出し側が安定した conversation identifier を渡す。
/// を渡す。`Request::cache_key` が `None` のときはキー自体を送らない。 /// `Request::cache_key` が `None` のときはキー自体を送らない。
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_key: Option<String>, pub prompt_cache_key: Option<String>,
} }
@ -74,10 +74,9 @@ pub(crate) struct ReasoningConfig {
#[serde(tag = "type", rename_all = "snake_case")] #[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum InputItem { pub(crate) enum InputItem {
/// 会話メッセージ。user / assistant / developer のいずれか。 /// 会話メッセージ。user / assistant / developer のいずれか。
/// `Role::System` items は `developer` として投影するChatGPT /// `Role::System` items は `developer` として投影する。OpenAI
/// backend が `role: "system"` を拒否するため。Codex CLI も /// Responses 互換 backend の一部は `role: "system"` を拒否するため、
/// system 相当の挿入には DeveloperInstructions = `role: "developer"` /// system 相当の挿入には `role: "developer"` を使う。
/// を使う)。
Message { Message {
role: &'static str, role: &'static str,
content: Vec<InputContent>, content: Vec<InputContent>,
@ -403,10 +402,9 @@ mod tests {
#[test] #[test]
fn system_role_item_is_projected_as_developer() { fn system_role_item_is_projected_as_developer() {
// ChatGPT backend (codex-oauth) は input[] の `role: "system"` を // Some compatible backends reject `role: "system"` in input[].
// "System messages are not allowed" で 400 拒否する。in-conversation // Project in-conversation system notes as `role: "developer"` so
// な system note (notify / fs_view auto-read / compaction summary) は // both official and compatible backends can accept them.
// `role: "developer"` として投影し、両 backend で受理されるようにする。
let scheme = OpenAIResponsesScheme::new(); let scheme = OpenAIResponsesScheme::new();
let req = Request::new() let req = Request::new()
.user("hi") .user("hi")
@ -523,11 +521,9 @@ mod tests {
fn reasoning_summary_field_is_always_serialized() { fn reasoning_summary_field_is_always_serialized() {
// Responses API は reasoning item に `summary` を必須で要求する。 // Responses API は reasoning item に `summary` を必須で要求する。
// summary が空でも wire 上に `summary: []` として残らないと、 // summary が空でも wire 上に `summary: []` として残らないと、
// ChatGPT backend (codex-oauth) が // backend によっては missing required parameter として拒否される。
// 400 invalid_request_error: Missing required parameter: // reasoning effort 未指定のターンでは summary text が付かないことが
// 'input[N].summary'. // あるため、空のままでも skip しないこと。
// で弾く。GPT-5 + reasoning effort 未指定のターンでは summary text
// が付かないことがあるため、空のままでも skip しないこと。
let scheme = OpenAIResponsesScheme::new(); let scheme = OpenAIResponsesScheme::new();
let item = Item::reasoning("").with_encrypted_content("ENC"); let item = Item::reasoning("").with_encrypted_content("ENC");
let req = Request::new().user("hi").item(item); let req = Request::new().user("hi").item(item);

View File

@ -20,9 +20,8 @@ impl Scheme for OpenAIResponsesScheme {
type State = OpenAIResponsesState; type State = OpenAIResponsesState;
fn default_base_url(&self) -> &'static str { fn default_base_url(&self) -> &'static str {
// `/v1` は base_url 側に寄せる。ChatGPT OAuth 経由のときは // `/v1` は base_url 側に寄せる。互換 backend を使う場合も、
// `https://chatgpt.com/backend-api/codex` を base にすれば同じ // base URL を差し替えるだけで同じ `/responses` path を使える。
// `/responses` path で両系統を吸収できるCodex CLI 準拠)。
"https://api.openai.com/v1" "https://api.openai.com/v1"
} }
@ -59,16 +58,18 @@ impl Scheme for OpenAIResponsesScheme {
fn validate_config(&self, config: &RequestConfig) -> Vec<ConfigWarning> { fn validate_config(&self, config: &RequestConfig) -> Vec<ConfigWarning> {
let mut warnings = Vec::new(); let mut warnings = Vec::new();
// ChatGPT backend (codex-oauth) は `max_output_tokens` を 400 で弾く。 // Some compatible backends reject `max_output_tokens` with HTTP 400.
// scheme 構築時に `send_max_output_tokens=false` で組まれていれば // If the scheme was built with `send_max_output_tokens=false`, body
// body 投影は止まっているので、ユーザの意図が落ちることだけを通知する。 // projection is already disabled; only notify that the user's intent
// was dropped.
if !self.send_max_output_tokens && config.max_tokens.is_some() { if !self.send_max_output_tokens && config.max_tokens.is_some() {
warnings.push(ConfigWarning::unsupported( warnings.push(ConfigWarning::unsupported(
"max_tokens", "max_tokens",
"OpenAI Responses (ChatGPT backend)", "OpenAI Responses (ChatGPT backend)",
)); ));
} }
// 同上、`temperature` / `top_p` も ChatGPT backend では 400 で弾かれる。 // Same for `temperature` / `top_p` on compatible backends that
// reject unsupported sampling parameters.
if !self.send_sampling_params { if !self.send_sampling_params {
if config.temperature.is_some() { if config.temperature.is_some() {
warnings.push(ConfigWarning::unsupported( warnings.push(ConfigWarning::unsupported(

View File

@ -28,11 +28,11 @@ use super::types::{Request, RequestConfig};
pub const DEFAULT_STREAM_OPEN_TIMEOUT: Duration = Duration::from_secs(20); pub const DEFAULT_STREAM_OPEN_TIMEOUT: Duration = Duration::from_secs(20);
pub const DEFAULT_FIRST_STREAM_EVENT_TIMEOUT: Duration = Duration::from_secs(30); pub const DEFAULT_FIRST_STREAM_EVENT_TIMEOUT: Duration = Duration::from_secs(30);
/// `AuthRef` を解決したランタイム表現。`crates/provider` が構築する /// 認証設定をリクエスト時に使える形へ解決したランタイム表現
/// ///
/// - `None`: 認証ヘッダを送らないOllama 等の opt-out /// - `None`: 認証ヘッダを送らないOllama 等の opt-out
/// - `ApiKey`: 静的な API key 文字列 /// - `ApiKey`: 静的な API key 文字列
/// - `Custom`: リクエスト毎に動的にヘッダを組み立てるCodex OAuth 等) /// - `Custom`: リクエスト毎に動的にヘッダを組み立てる
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ResolvedAuth { pub enum ResolvedAuth {
None, None,
@ -226,9 +226,8 @@ impl<S: Scheme> HttpTransport<S> {
let value = HeaderValue::from_str(cache_key).map_err(|e| { let value = HeaderValue::from_str(cache_key).map_err(|e| {
ClientError::Config(format!("invalid Codex conversation header: {e}")) ClientError::Config(format!("invalid Codex conversation header: {e}"))
})?; })?;
// Codex CLI sends hyphenated session/thread headers to the // Send both current hyphenated conversation headers and the
// ChatGPT Codex backend. Keep the legacy underscore header for // legacy underscore form for compatibility with existing backends.
// existing traces/backends while exposing the current Codex shape.
headers.insert(HeaderName::from_static("session-id"), value.clone()); headers.insert(HeaderName::from_static("session-id"), value.clone());
headers.insert(HeaderName::from_static("thread-id"), value.clone()); headers.insert(HeaderName::from_static("thread-id"), value.clone());
headers.insert(HeaderName::from_static("session_id"), value.clone()); headers.insert(HeaderName::from_static("session_id"), value.clone());

View File

@ -1,6 +1,6 @@
//! LLM Client Common Types //! LLM Client Common Types
//! //!
//! Core conversation types for yoi's LLM interaction model. //! Core conversation types for LLM interaction.
//! The core abstraction is `Item` which represents different types of conversation elements: //! The core abstraction is `Item` which represents different types of conversation elements:
//! - Message items (user/assistant messages with content parts) //! - Message items (user/assistant messages with content parts)
//! - ToolCall items (tool invocations) //! - ToolCall items (tool invocations)
@ -521,9 +521,8 @@ pub struct Request {
/// Providers without prompt caching ignore the field. /// Providers without prompt caching ignore the field.
pub cache_anchor: Option<usize>, pub cache_anchor: Option<usize>,
/// 会話単位の安定キー。`prompt_cache_key` として送られる /// 会話単位の安定キー。`prompt_cache_key` として送られる
/// (OpenAI Responses)。ChatGPT backend (codex-oauth) は明示キーが /// (OpenAI Responses)。明示キーを必要とする backend では、
/// 無いと org/project ハッシュ衝突でプロンプトキャッシュが /// 呼び出し側が安定した conversation identifier を渡す。
/// ほぼヒットしないため、worker 側で `SegmentId` を渡す運用を想定。
/// `cache_anchor` と違い名前空間キーであり、`prefix anchor` とは /// `cache_anchor` と違い名前空間キーであり、`prefix anchor` とは
/// 別の概念。`cache_anchor` を読まない provider と同じく、 /// 別の概念。`cache_anchor` を読まない provider と同じく、
/// `prompt_cache_key` を持たない provider は無視する。 /// `prompt_cache_key` を持たない provider は無視する。

View File

@ -8,8 +8,8 @@
//! //!
//! Prune は **コンテキスト射影** であり、history の変換ではない。 //! Prune は **コンテキスト射影** であり、history の変換ではない。
//! この crate が提供するのは pure な候補抽出 [`prunable_indices`] のみで、 //! この crate が提供するのは pure な候補抽出 [`prunable_indices`] のみで、
//! 射影の適用は上位層(`worker::prune_hook` 等)が LLM に送る一時コンテキスト //! 射影の適用は LLM に送る一時コンテキストに対してだけ行う。
//! に対してだけ行う。Engine の永続履歴は決して変更されない。 //! Engine の永続履歴は決して変更されない。
//! //!
//! 保護境界は末尾 token budget で決めるが、この crate は usage 履歴を //! 保護境界は末尾 token budget で決めるが、この crate は usage 履歴を
//! 所有しない。prefix ごとの token 推定値と savings 推定は上位層から //! 所有しない。prefix ごとの token 推定値と savings 推定は上位層から
@ -75,7 +75,7 @@ pub enum PruneDecision {
} }
/// Optional observer invoked after each prune evaluation, regardless of /// Optional observer invoked after each prune evaluation, regardless of
/// branch. Worker 等の上位層が install して metrics を発行する。 /// branch. Callers can install this to publish metrics or traces.
pub type PruneObserver = Box<dyn Fn(&PruneEvaluation) + Send + Sync>; pub type PruneObserver = Box<dyn Fn(&PruneEvaluation) + Send + Sync>;
/// Configuration for the Prune algorithm. /// Configuration for the Prune algorithm.

View File

@ -75,10 +75,9 @@ impl ToolServerHandle {
/// Execute all pending factories and register the resulting tools. /// Execute all pending factories and register the resulting tools.
/// ///
/// Called implicitly by `Engine::lock()` before the first turn. /// Called implicitly by `Engine::lock()` before the first turn.
/// Exposed as `pub` so higher layers (e.g. Worker) can force-materialise /// Exposed as `pub` so callers can force-materialise tools earlier —
/// tools earlier — for example when building a system-prompt template /// for example when building a system-prompt template context that needs
/// context that needs the list of registered tool names. Redundant /// the list of registered tool names. Redundant calls are no-ops.
/// calls are no-ops.
/// ///
/// # Panics /// # Panics
/// ///

View File

@ -2,9 +2,8 @@
//! //!
//! 1 リクエストの送信時点での「ある history prefix 長で計測した占有量」を //! 1 リクエストの送信時点での「ある history prefix 長で計測した占有量」を
//! 1 件分にまとめたもの。`UsageEvent` (provider stream イベント) を //! 1 件分にまとめたもの。`UsageEvent` (provider stream イベント) を
//! 受けて呼び出し側 (typically Worker) が組み立て、永続化層 //! 受けて呼び出し側が組み立て、永続化層に流したり、token accounting
//! (session-store) に流したり、token accounting (`token_counter`) で //! (`token_counter`) で履歴として参照したりする。
//! 履歴として参照したりする。
/// LLM リクエスト送信時点での占有量スナップショット。 /// LLM リクエスト送信時点での占有量スナップショット。
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]