docs: generalize llm-engine comments

This commit is contained in:
2026-07-14 04:14:58 +09:00
parent 34856b9900
commit 1505c5e8a3
16 changed files with 93 additions and 111 deletions
+11 -17
View File
@@ -1,15 +1,11 @@
//! `Scheme` 実装と通信層が要求する認証要件、および動的認証プロバイダ。
//!
//! マニフェスト側の型(`ModelConfig` / `SchemeKind` / `AuthRef`)は
//! `crates/manifest` に置き、llm-engine はそれを知らずに済む。
//! `AuthRequirement` は scheme が宣言する「この scheme はどんな認証を
//! 期待するか」のランタイム記述で、manifest 側の `AuthRef` との
//! 照合(`AuthRef → ResolvedAuth` 変換の適否)は `crates/provider`
//! で行う。
//! 期待するか」のランタイム記述で、設定ファイルや環境変数などから
//! [`super::transport::ResolvedAuth`] を組み立てる責務は呼び出し側にある。
//!
//! Codex OAuth のようにリクエスト毎にトークンが変わり得る認証は
//! [`AuthProvider`] trait を `crates/provider` 側で実装し、
//! [`super::transport::ResolvedAuth::Custom`] 経由で transport に渡す。
//! リクエスト毎にトークンが変わり得る認証は [`AuthProvider`] trait を
//! 実装し、[`super::transport::ResolvedAuth::Custom`] 経由で transport に渡す。
use async_trait::async_trait;
use reqwest::header::{HeaderName, HeaderValue};
@@ -27,16 +23,15 @@ pub enum AuthRequirement {
XApiKey,
/// クエリパラメータ `?<name>=<token>`Gemini 形式)
QueryParam { name: &'static str },
/// 複合ヘッダ(Codex OAuth 等、`crates/provider` で解決)
/// 複合ヘッダ(呼び出し側が [`AuthProvider`] で解決)
Custom,
}
/// リクエスト毎に認証ヘッダを動的に組み立てるプロバイダ。
///
/// Codex OAuth のように access_token が refresh で更新されたり、
/// `ChatGPT-Account-Id` / `X-OpenAI-Fedramp` のような複数ヘッダを
/// 同時に注入する必要があるケースで使う。実体は `crates/provider`
/// 側に置き、llm-engine は trait を知るだけ。
/// access token が refresh で更新されたり、複数ヘッダを同時に注入する
/// 必要があるケースで使う。実体は呼び出し側に置き、llm-engine は
/// trait を知るだけ。
///
/// 返したヘッダはそのまま `HeaderMap` に挿入される。`Authorization`
/// 含む scheme 既定の認証ヘッダは送出されないので、必要なら
@@ -46,11 +41,10 @@ pub trait AuthProvider: Send + Sync + std::fmt::Debug {
/// 1 リクエスト分の認証ヘッダを返す。refresh が必要なら内部で行う。
async fn headers(&self) -> Result<Vec<(HeaderName, HeaderValue)>, ClientError>;
/// ChatGPT Codex backend 向けの複合認証かどうか。
/// Conversation header / request compression が必要な backend profile かどうか。
///
/// transport は provider crate の具象型を知らないため、この hook だけで
/// Codex CLI 互換の wire behaviorconversation header / request compression 等)
/// を切り替える。
/// transport は呼び出し側の具象型を知らないため、この hook だけで
/// 追加の wire behavior を切り替える。
fn is_codex_backend(&self) -> bool {
false
}
@@ -45,8 +45,8 @@ pub trait Scheme: Clone + Send + Sync + 'static {
/// プロバイダもあるため、モデル ID を受け取る。
fn path(&self, model_id: &str) -> String;
/// この scheme が要求する認証形式。`build_client` 時に
/// `manifest::AuthRef` と照合する。
/// この scheme が要求する認証形式。呼び出し側は client 構築時に
/// 設定された認証情報と照合する。
fn required_auth(&self) -> AuthRequirement;
/// `Content-Type` 以外の追加ヘッダ。`anthropic-version` / `anthropic-beta` 等。
@@ -78,8 +78,7 @@ pub trait Scheme: Clone + Send + Sync + 'static {
/// scheme 既定の capability。モデル ID に関係なく、この wire で
/// 安全に送れる最小共通項を返す。既知モデル ID の能力テーブルは
/// `provider::capability::lookup` 側(高レベル構築層)の責務で、
/// scheme はここには関与しない。
/// 高レベルの client 構築層の責務で、scheme はここには関与しない。
fn default_capability(&self) -> ModelCapability;
/// scheme 側でサポートしていない `RequestConfig` フィールドを
@@ -1,7 +1,7 @@
//! OpenAI Responses scheme の wire-level 既定 capability。
//!
//! モデル ID 固有のテーブル(`gpt-5` / `codex-` 系など)は高レベル構築層
//! (`provider::capability`)の責務。ここでは wire の保守的 default のみ。
//! モデル ID 固有の能力テーブルは高レベルの client 構築層の責務。
//! ここでは wire の保守的 default のみ。
use crate::llm_client::capability::{
CacheStrategy, ModelCapability, StructuredOutput, ToolCallingSupport,
@@ -2,7 +2,7 @@
//!
//! `response.*` 名前空間の SSE を共通の [`Event`](crate::llm_client::event::Event)
//! に変換する。Responses の (output_index, content_index) 2 次元座標と
//! yoi 側 1 次元 `BlockStart/Delta/Stop::index` のマッピングは
//! この crate の 1 次元 `BlockStart/Delta/Stop::index` のマッピングは
//! [`OpenAIResponsesState`] が保持する。
use std::collections::{BTreeMap, HashMap};
@@ -2,7 +2,7 @@
//!
//! Chat Completions とは別物の item-based wire format。reasoning item と
//! function_call item が first-class で、SSE イベントも `response.*` 名前空間で
//! 流れる。ChatGPT OAuth 経路 (codex) は本 scheme 必須。
//! 流れる。
//!
//! - リクエスト JSON 生成: [`request`]
//! - 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` /
/// `send_sampling_params` は scheme 固定の wire 設定で、デフォルトは
/// 公式 OpenAI Responses API 向け (stateless + ZDR + `max_output_tokens`
/// / `temperature` / `top_p` 送出可)。ChatGPT backend (codex-oauth)
/// ように受理パラメータが subset の経路では provider 層で
/// `send_max_output_tokens=false` / `send_sampling_params=false` に
/// 上書きする。`ModelCapability` には入れない(モデル能力ではなく wire policy)。
/// / `temperature` / `top_p` 送出可)。受理パラメータが subset
/// 互換 backend では client 構築層で `send_max_output_tokens=false` /
/// `send_sampling_params=false` に上書きする。`ModelCapability` には
/// 入れない(モデル能力ではなく wire policy)。
#[derive(Debug, Clone)]
pub struct OpenAIResponsesScheme {
/// サーバ側に response を保存するか。ZDR/stateless 運用では `false`。
@@ -31,12 +31,12 @@ pub struct OpenAIResponsesScheme {
/// `store=false` で reasoning を使うなら必須。
pub include_encrypted_content: bool,
/// `max_output_tokens` を body に載せるか。公式 OpenAI Responses API は
/// 受理するが、ChatGPT backend (codex-oauth) は `Unsupported parameter`
/// 400 を返すため、その経路では `false` にする。
/// 受理するが、互換 backend によっては `Unsupported parameter`
/// 400 を返すため、その経路では `false` にする。
pub send_max_output_tokens: bool,
/// `temperature` / `top_p` を body に載せるか。公式 OpenAI Responses API
/// は受理するが、ChatGPT backend (codex-oauth) は `Unsupported parameter`
/// 400 を返すため、その経路では `false` にする。
/// は受理するが、互換 backend によっては `Unsupported parameter`
/// 400 を返すため、その経路では `false` にする。
pub send_sampling_params: bool,
}
@@ -38,21 +38,21 @@ pub(crate) struct ResponsesRequest {
/// `["reasoning.encrypted_content"]` 等。
#[serde(skip_serializing_if = "Vec::is_empty")]
pub include: Vec<&'static str>,
/// 公式 OpenAI Responses API では受理されるが、ChatGPT backend
/// (codex-oauth) は 400 で弾く。scheme の `send_max_output_tokens`
/// が `false` のときは `None` のまま送る (skip_serializing_if で除外)。
/// 公式 OpenAI Responses API では受理されるが、互換 backend によっては
/// 400 で弾く。scheme の `send_max_output_tokens` が `false` のときは
/// `None` のまま送る (skip_serializing_if で除外)。
#[serde(skip_serializing_if = "Option::is_none")]
pub max_output_tokens: Option<u32>,
/// 公式 OpenAI Responses API では受理されるが、ChatGPT backend
/// (codex-oauth) は `temperature` / `top_p` を 400 で弾く。scheme の
/// 公式 OpenAI Responses API では受理されるが、互換 backend によっては
/// `temperature` / `top_p` を 400 で弾く。scheme の
/// `send_sampling_params` が `false` のときは `None` のまま送る。
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub top_p: Option<f32>,
/// 会話単位の安定キー。ChatGPT backend (codex-oauth) は明示キーが
/// 無いとプロンプトキャッシュがほぼ効かない。worker 側は `SegmentId`
/// を渡す。`Request::cache_key` が `None` のときはキー自体を送らない。
/// 会話単位の安定キー。明示キーを必要とする backend では、
/// 呼び出し側が安定した conversation identifier を渡す。
/// `Request::cache_key` が `None` のときはキー自体を送らない。
#[serde(skip_serializing_if = "Option::is_none")]
pub prompt_cache_key: Option<String>,
}
@@ -74,10 +74,9 @@ pub(crate) struct ReasoningConfig {
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum InputItem {
/// 会話メッセージ。user / assistant / developer のいずれか。
/// `Role::System` items は `developer` として投影するChatGPT
/// backend `role: "system"` を拒否するため。Codex CLI も
/// system 相当の挿入には DeveloperInstructions = `role: "developer"`
/// を使う)。
/// `Role::System` items は `developer` として投影する。OpenAI
/// Responses 互換 backend の一部は `role: "system"` を拒否するため
/// system 相当の挿入には `role: "developer"` を使う。
Message {
role: &'static str,
content: Vec<InputContent>,
@@ -403,10 +402,9 @@ mod tests {
#[test]
fn system_role_item_is_projected_as_developer() {
// ChatGPT backend (codex-oauth) は input[] の `role: "system"`
// "System messages are not allowed" で 400 拒否する。in-conversation
// な system note (notify / fs_view auto-read / compaction summary) は
// `role: "developer"` として投影し、両 backend で受理されるようにする。
// Some compatible backends reject `role: "system"` in input[].
// Project in-conversation system notes as `role: "developer"` so
// both official and compatible backends can accept them.
let scheme = OpenAIResponsesScheme::new();
let req = Request::new()
.user("hi")
@@ -523,11 +521,9 @@ mod tests {
fn reasoning_summary_field_is_always_serialized() {
// Responses API は reasoning item に `summary` を必須で要求する。
// summary が空でも wire 上に `summary: []` として残らないと、
// ChatGPT backend (codex-oauth) が
// 400 invalid_request_error: Missing required parameter:
// 'input[N].summary'.
// で弾く。GPT-5 + reasoning effort 未指定のターンでは summary text
// が付かないことがあるため、空のままでも skip しないこと。
// backend によっては missing required parameter として拒否される。
// reasoning effort 未指定のターンでは summary text が付かないことが
// あるため、空のままでも skip しないこと。
let scheme = OpenAIResponsesScheme::new();
let item = Item::reasoning("").with_encrypted_content("ENC");
let req = Request::new().user("hi").item(item);
@@ -20,9 +20,8 @@ impl Scheme for OpenAIResponsesScheme {
type State = OpenAIResponsesState;
fn default_base_url(&self) -> &'static str {
// `/v1` は base_url 側に寄せる。ChatGPT OAuth 経由のときは
// `https://chatgpt.com/backend-api/codex` を base にすれば同じ
// `/responses` path で両系統を吸収できる(Codex CLI 準拠)。
// `/v1` は base_url 側に寄せる。互換 backend を使う場合も、
// base URL を差し替えるだけで同じ `/responses` path を使える。
"https://api.openai.com/v1"
}
@@ -59,16 +58,18 @@ impl Scheme for OpenAIResponsesScheme {
fn validate_config(&self, config: &RequestConfig) -> Vec<ConfigWarning> {
let mut warnings = Vec::new();
// ChatGPT backend (codex-oauth) は `max_output_tokens` を 400 で弾く。
// scheme 構築時に `send_max_output_tokens=false` で組まれていれば
// body 投影は止まっているので、ユーザの意図が落ちることだけを通知する。
// Some compatible backends reject `max_output_tokens` with HTTP 400.
// If the scheme was built with `send_max_output_tokens=false`, 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() {
warnings.push(ConfigWarning::unsupported(
"max_tokens",
"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 config.temperature.is_some() {
warnings.push(ConfigWarning::unsupported(
@@ -28,11 +28,11 @@ use super::types::{Request, RequestConfig};
pub const DEFAULT_STREAM_OPEN_TIMEOUT: Duration = Duration::from_secs(20);
pub const DEFAULT_FIRST_STREAM_EVENT_TIMEOUT: Duration = Duration::from_secs(30);
/// `AuthRef` を解決したランタイム表現。`crates/provider` が構築する
/// 認証設定をリクエスト時に使える形へ解決したランタイム表現
///
/// - `None`: 認証ヘッダを送らない(Ollama 等の opt-out
/// - `ApiKey`: 静的な API key 文字列
/// - `Custom`: リクエスト毎に動的にヘッダを組み立てるCodex OAuth 等)
/// - `Custom`: リクエスト毎に動的にヘッダを組み立てる
#[derive(Debug, Clone)]
pub enum ResolvedAuth {
None,
@@ -226,9 +226,8 @@ impl<S: Scheme> HttpTransport<S> {
let value = HeaderValue::from_str(cache_key).map_err(|e| {
ClientError::Config(format!("invalid Codex conversation header: {e}"))
})?;
// Codex CLI sends hyphenated session/thread headers to the
// ChatGPT Codex backend. Keep the legacy underscore header for
// existing traces/backends while exposing the current Codex shape.
// Send both current hyphenated conversation headers and the
// legacy underscore form for compatibility with existing backends.
headers.insert(HeaderName::from_static("session-id"), value.clone());
headers.insert(HeaderName::from_static("thread-id"), value.clone());
headers.insert(HeaderName::from_static("session_id"), value.clone());
+3 -4
View File
@@ -1,6 +1,6 @@
//! 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:
//! - Message items (user/assistant messages with content parts)
//! - ToolCall items (tool invocations)
@@ -521,9 +521,8 @@ pub struct Request {
/// Providers without prompt caching ignore the field.
pub cache_anchor: Option<usize>,
/// 会話単位の安定キー。`prompt_cache_key` として送られる
/// (OpenAI Responses)。ChatGPT backend (codex-oauth) は明示キーが
/// 無いと org/project ハッシュ衝突でプロンプトキャッシュが
/// ほぼヒットしないため、worker 側で `SegmentId` を渡す運用を想定。
/// (OpenAI Responses)。明示キーを必要とする backend では、
/// 呼び出し側が安定した conversation identifier を渡す。
/// `cache_anchor` と違い名前空間キーであり、`prefix anchor` とは
/// 別の概念。`cache_anchor` を読まない provider と同じく、
/// `prompt_cache_key` を持たない provider は無視する。