diff --git a/Cargo.lock b/Cargo.lock index b0bcade5..51548ce5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -548,16 +548,6 @@ dependencies = [ "unicode-segmentation", ] -[[package]] -name = "core-foundation" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "core-foundation" version = "0.10.1" @@ -1668,11 +1658,9 @@ dependencies = [ "percent-encoding", "pin-project-lite", "socket2", - "system-configuration", "tokio", "tower-service", "tracing", - "windows-registry", ] [[package]] @@ -2108,6 +2096,8 @@ name = "llm-engine" version = "0.2.1" dependencies = [ "async-trait", + "base64", + "chrono", "clap", "dotenv", "eventsource-stream", @@ -2231,6 +2221,7 @@ dependencies = [ "serde", "serde_ignored", "serde_json", + "serial_test", "sha2 0.10.9", "tempfile", "thiserror 2.0.18", @@ -2907,28 +2898,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "provider" -version = "0.1.0" -dependencies = [ - "async-trait", - "base64", - "chrono", - "llm-engine", - "manifest", - "reqwest", - "secrets", - "serde", - "serde_json", - "serial_test", - "tempfile", - "thiserror 2.0.18", - "tokio", - "toml", - "tracing", - "wiremock", -] - [[package]] name = "pulldown-cmark" version = "0.13.3" @@ -3291,7 +3260,6 @@ checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" dependencies = [ "base64", "bytes", - "encoding_rs", "futures-channel", "futures-core", "futures-util", @@ -3305,7 +3273,6 @@ dependencies = [ "hyper-util", "js-sys", "log", - "mime", "native-tls", "percent-encoding", "pin-project-lite", @@ -3447,7 +3414,7 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "jni", "log", @@ -3501,15 +3468,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scc" -version = "2.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" version = "0.1.29" @@ -3550,12 +3508,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sdd" -version = "3.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca" - [[package]] name = "secrets" version = "0.1.0" @@ -3574,7 +3526,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" dependencies = [ "bitflags 2.11.0", - "core-foundation 0.10.1", + "core-foundation", "core-foundation-sys", "libc", "security-framework-sys", @@ -3711,24 +3663,23 @@ dependencies = [ [[package]] name = "serial_test" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f" +checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d" dependencies = [ "futures-executor", "futures-util", "log", "once_cell", "parking_lot", - "scc", "serial_test_derive", ] [[package]] name = "serial_test_derive" -version = "3.4.0" +version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9" +checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c" dependencies = [ "proc-macro2", "quote", @@ -4016,27 +3967,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "system-configuration" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13f3d0daba03132c0aa9767f98351b3488edc2c100cda2d2ec2b04f3d8d3c8b" -dependencies = [ - "bitflags 2.11.0", - "core-foundation 0.9.4", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tar" version = "0.4.46" @@ -4602,7 +4532,6 @@ dependencies = [ "manifest", "minijinja", "protocol", - "provider", "pulldown-cmark", "ratatui", "secrets", @@ -5460,17 +5389,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-registry" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" -dependencies = [ - "windows-link", - "windows-result", - "windows-strings", -] - [[package]] name = "windows-result" version = "0.4.1" @@ -5804,11 +5722,12 @@ dependencies = [ "memory", "minijinja", "protocol", - "provider", "reqwest", "schemars", + "secrets", "serde", "serde_json", + "serial_test", "session-metrics", "session-store", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 3b5cf63c..5fc2dab8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,6 @@ members = [ "crates/plugin-pdk", "crates/yoi", "crates/protocol", - "crates/provider", "crates/session-metrics", "crates/session-analytics", "crates/lint-common", @@ -39,7 +38,6 @@ default-members = [ "crates/plugin-pdk", "crates/yoi", "crates/protocol", - "crates/provider", "crates/session-metrics", "crates/session-analytics", "crates/lint-common", @@ -72,7 +70,6 @@ worker-runtime = { path = "crates/worker-runtime" } yoi-plugin-pdk = { path = "crates/plugin-pdk" } yoi = { path = "crates/yoi" } protocol = { path = "crates/protocol" } -provider = { path = "crates/provider" } session-metrics = { path = "crates/session-metrics" } session-analytics = { path = "crates/session-analytics" } session-store = { path = "crates/session-store" } diff --git a/crates/llm-engine-macros/README.md b/crates/llm-engine-macros/README.md index a7cf7b99..733862d8 100644 --- a/crates/llm-engine-macros/README.md +++ b/crates/llm-engine-macros/README.md @@ -20,7 +20,7 @@ Does not own: ## 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 diff --git a/crates/llm-engine/Cargo.toml b/crates/llm-engine/Cargo.toml index c588e268..6fbac462 100644 --- a/crates/llm-engine/Cargo.toml +++ b/crates/llm-engine/Cargo.toml @@ -5,6 +5,10 @@ version = "0.2.1" edition.workspace = true license.workspace = true +[features] +default = [] +codex = ["dep:base64", "dep:chrono"] + [dependencies] serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } @@ -12,11 +16,13 @@ thiserror = { workspace = true } tracing = { workspace = true } async-trait = { workspace = true } futures = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread", "time"] } +tokio = { workspace = true, features = ["fs", "macros", "rt-multi-thread", "sync", "time"] } tokio-util = "0.7" reqwest = { version = "0.13", default-features = false, features = ["stream", "json", "native-tls", "http2"] } eventsource-stream = "0.2" zstd = "0.13" +base64 = { version = "0.22.1", optional = true } +chrono = { version = "0.4", default-features = false, features = ["serde", "clock"], optional = true } llm-engine-macros = { workspace = true } [dev-dependencies] diff --git a/crates/llm-engine/README.md b/crates/llm-engine/README.md index 19db5846..57320289 100644 --- a/crates/llm-engine/README.md +++ b/crates/llm-engine/README.md @@ -16,10 +16,10 @@ Owns: Does not own: -- Worker names, sockets, process lifecycle, or scope delegation (`worker`) -- product CLI shape (`yoi`) -- provider catalog and secret resolution (`provider`, `secrets`) -- durable Worker current state (`session-store` worker metadata) +- Host application names, sockets, process lifecycle, or scope delegation +- Product CLI shape +- Provider catalog and secret resolution +- Durable application state outside engine history ## Design notes diff --git a/crates/llm-engine/src/engine.rs b/crates/llm-engine/src/engine.rs index fa92a651..d41d8631 100644 --- a/crates/llm-engine/src/engine.rs +++ b/crates/llm-engine/src/engine.rs @@ -213,9 +213,9 @@ pub struct Engine { /// stream events become visible. lifecycle_trace_cbs: Vec>, /// Non-fatal warning callbacks. Invoked when the Engine wants to - /// surface an advisory message to the upper layer (e.g. Worker) so it - /// can be forwarded to the user — distinct from `tracing::warn!`, - /// which is for developer-facing logs. + /// surface an advisory message to the caller so it can be forwarded + /// to the user — distinct from `tracing::warn!`, which is for + /// developer-facing logs. warning_cbs: Vec>, /// Tool-result callbacks. Invoked once per completed tool call /// after post-execution interceptors and the output byte-cap @@ -253,8 +253,8 @@ pub struct Engine { /// Plumbed into [`Request::cache_anchor`] at request build time. cache_anchor: Option, /// Conversation-scoped cache key, set by higher layers. Plumbed into - /// [`Request::cache_key`] at request build time. Worker 側では - /// `SegmentId` を渡す。 + /// [`Request::cache_key`] at request build time. Callers should pass a + /// stable conversation identifier when the backend benefits from one. cache_key: Option, /// State marker _state: PhantomData, @@ -486,8 +486,8 @@ impl Engine { /// /// Fired after `post_tool_call` interceptors and any `content` /// truncation from `tool_output_limits`, so the callback observes - /// exactly what is persisted to history. Intended for upper layers - /// (e.g. Worker) to forward tool results to clients. + /// exactly what is persisted to history. Intended for callers that need + /// to forward tool results to clients. pub fn on_tool_result(&mut self, callback: impl Fn(&ToolResult) + Send + Sync + 'static) { self.tool_result_cbs.push(Box::new(callback)); } @@ -1121,10 +1121,10 @@ impl Engine { } // Drain interceptor-side inputs that are meant to land in - // history (notifications, cross-Worker events, system - // reminders). These are committed *before* the per-request - // clone so they participate in the LLM request below and - // get persisted by the upper layer that owns history.json. + // history (notifications, external events, system reminders). + // These are committed *before* the per-request clone so they + // participate in the LLM request below and get persisted by + // the caller that owns durable history. let pending = self.interceptor.pending_history_appends().await; if !pending.is_empty() { self.append_history_items(pending); @@ -1301,9 +1301,7 @@ impl Engine { self.turn_count += 1; // Collect and commit assistant items. Routed through - // `append_history_items` so observers (e.g. the - // Worker-side per-item session-log committer) see each item - // as it lands. + // `append_history_items` so observers see each item as it lands. let reasoning_items = self.thinking_block_collector.take_collected(); let text_blocks = self.text_block_collector.take_collected(); let tool_calls = self.tool_call_collector.take_collected(); @@ -1603,8 +1601,7 @@ impl Engine { } Ok(ToolExecutionResult::Completed(results)) => { // Route per-result pushes through the callback path so - // observers (e.g. the Worker-side per-item session-log - // committer) see each tool result as it lands. + // observers see each tool result as it lands. let items = results.into_iter().map(|result| { Item::tool_result_item( &result.tool_use_id, @@ -1707,9 +1704,9 @@ impl Engine { /// Install byte-size caps for tool execution `content`. /// - /// Passing `None` (the default) disables truncation. Higher layers - /// (e.g. Worker) translate manifest configuration into a concrete - /// [`ToolOutputLimits`] and install it here. + /// Passing `None` (the default) disables truncation. Callers translate + /// their own configuration into a concrete [`ToolOutputLimits`] and + /// install it here. pub fn set_tool_output_limits(&mut self, limits: Option) { self.tool_output_limits = limits; } diff --git a/crates/llm-engine/src/interceptor.rs b/crates/llm-engine/src/interceptor.rs index ae246e7d..3e088ced 100644 --- a/crates/llm-engine/src/interceptor.rs +++ b/crates/llm-engine/src/interceptor.rs @@ -1,9 +1,8 @@ //! Interceptor - control flow delegation for the Engine execution loop //! -//! Defines the [`Interceptor`] trait that upper layers (e.g. Worker) implement -//! to inject orchestration decisions (approval, skip, pause, abort) -//! into the Engine's turn loop without the Engine knowing about -//! higher-level concepts. +//! Defines the [`Interceptor`] trait that callers implement to inject +//! orchestration decisions (approval, skip, pause, abort) into the Engine's +//! turn loop without the Engine knowing about host-application concepts. use std::sync::Arc; @@ -132,8 +131,8 @@ pub struct ToolResultInfo { /// Intercepts the Engine execution loop at key decision points. /// /// All methods have default implementations that let the Engine -/// proceed without intervention. Upper layers (e.g. Worker) provide -/// richer implementations for approval flows, permission checks, etc. +/// proceed without intervention. Callers provide richer implementations for +/// approval flows, permission checks, etc. #[async_trait] pub trait Interceptor: Send + Sync { /// 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 /// 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 /// per-request clone, so any committed assistant response that /// reacts to the injection would have no visible trigger on the diff --git a/crates/llm-engine/src/lib.rs b/crates/llm-engine/src/lib.rs index 02766e91..96f8cb59 100644 --- a/crates/llm-engine/src/lib.rs +++ b/crates/llm-engine/src/lib.rs @@ -45,6 +45,7 @@ pub(crate) mod callback; pub mod event; pub mod interceptor; pub mod llm_client; +pub mod providers; pub mod prune; pub mod state; pub mod timeline; diff --git a/crates/llm-engine/src/llm_client/auth.rs b/crates/llm-engine/src/llm_client/auth.rs index ba48fd6c..e35f4fff 100644 --- a/crates/llm-engine/src/llm_client/auth.rs +++ b/crates/llm-engine/src/llm_client/auth.rs @@ -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, /// クエリパラメータ `?=`(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 既定の認証ヘッダは送出されないので、必要なら @@ -45,13 +40,4 @@ pub enum AuthRequirement { pub trait AuthProvider: Send + Sync + std::fmt::Debug { /// 1 リクエスト分の認証ヘッダを返す。refresh が必要なら内部で行う。 async fn headers(&self) -> Result, ClientError>; - - /// ChatGPT Codex backend 向けの複合認証かどうか。 - /// - /// transport は provider crate の具象型を知らないため、この hook だけで - /// Codex CLI 互換の wire behavior(conversation header / request compression 等) - /// を切り替える。 - fn is_codex_backend(&self) -> bool { - false - } } diff --git a/crates/llm-engine/src/llm_client/retry.rs b/crates/llm-engine/src/llm_client/retry.rs index 77cb03a0..acaa88df 100644 --- a/crates/llm-engine/src/llm_client/retry.rs +++ b/crates/llm-engine/src/llm_client/retry.rs @@ -1,15 +1,15 @@ //! LLM response stream を開く前の transient error 向けリトライポリシー。 //! -//! Engine が `LlmClient::stream` の open error に対して `is_retryable` を見て -//! retry / backoff / TUI event / cancellation をまとめて管理する。 +//! `LlmClient::stream` の open error に対して `is_retryable` を見て +//! retry / backoff / cancellation をまとめて管理する。 //! SSE 読み出し開始後の失敗は対象外。 use std::time::Duration; /// 指数バックオフ + ジッター + 累積タイムアウトを表すポリシー。 /// -/// `Default` は llm-engine 全体の固定値を返す。manifest 経由の上書きが -/// 必要になったら拡張する(現状は不要 → `tickets/llm-engine-transient-retry.md`)。 +/// `Default` は llm-engine 全体の固定値を返す。呼び出し側からの上書きが +/// 必要になったら拡張する。 #[derive(Debug, Clone)] pub struct RetryPolicy { /// 指数の基準値。`base * 2^attempt` を `cap` で頭打ちにした上限から diff --git a/crates/llm-engine/src/llm_client/scheme/anthropic/capability.rs b/crates/llm-engine/src/llm_client/scheme/anthropic/capability.rs index ccfdc524..3a81974d 100644 --- a/crates/llm-engine/src/llm_client/scheme/anthropic/capability.rs +++ b/crates/llm-engine/src/llm_client/scheme/anthropic/capability.rs @@ -1,7 +1,7 @@ //! Anthropic scheme の wire-level 既定 capability。 //! -//! モデル ID 固有のテーブル(`claude-*` など)は高レベル構築層 -//! (`provider::capability`)の責務。ここでは未知モデルでも「この wire で +//! モデル ID 固有のテーブル(`claude-*` など)は client construction layer +//! の責務。ここでは未知モデルでも「この wire で //! 安全に送れる最小共通項」を返すだけに留める。 use crate::llm_client::capability::{ diff --git a/crates/llm-engine/src/llm_client/scheme/gemini/capability.rs b/crates/llm-engine/src/llm_client/scheme/gemini/capability.rs index 315cf06e..4f0c3a91 100644 --- a/crates/llm-engine/src/llm_client/scheme/gemini/capability.rs +++ b/crates/llm-engine/src/llm_client/scheme/gemini/capability.rs @@ -1,7 +1,7 @@ //! Gemini scheme の wire-level 既定 capability。 //! //! モデル ID 固有のテーブル(`gemini-*` バージョン別の reasoning 有無)は -//! 高レベル構築層(`provider::capability`)の責務。ここでは wire の +//! client construction layer の責務。ここでは wire の //! 保守的 default のみ。 use crate::llm_client::capability::{ diff --git a/crates/llm-engine/src/llm_client/scheme/mod.rs b/crates/llm-engine/src/llm_client/scheme/mod.rs index ecea20c2..8306b32d 100644 --- a/crates/llm-engine/src/llm_client/scheme/mod.rs +++ b/crates/llm-engine/src/llm_client/scheme/mod.rs @@ -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` フィールドを diff --git a/crates/llm-engine/src/llm_client/scheme/openai_chat/capability.rs b/crates/llm-engine/src/llm_client/scheme/openai_chat/capability.rs index b89784bc..69171e3e 100644 --- a/crates/llm-engine/src/llm_client/scheme/openai_chat/capability.rs +++ b/crates/llm-engine/src/llm_client/scheme/openai_chat/capability.rs @@ -1,7 +1,7 @@ //! OpenAI Chat Completions scheme の wire-level 既定 capability。 //! -//! モデル ID 固有のテーブル(`gpt-5` 系など)は高レベル構築層 -//! (`provider::capability`)の責務。ここでは wire の保守的 default のみ。 +//! モデル ID 固有のテーブル(`gpt-5` 系など)は client construction layer +//! の責務。ここでは wire の保守的 default のみ。 use crate::llm_client::capability::{ CacheStrategy, ModelCapability, StructuredOutput, ToolCallingSupport, diff --git a/crates/llm-engine/src/llm_client/scheme/openai_responses/capability.rs b/crates/llm-engine/src/llm_client/scheme/openai_responses/capability.rs index d6257163..ce1df462 100644 --- a/crates/llm-engine/src/llm_client/scheme/openai_responses/capability.rs +++ b/crates/llm-engine/src/llm_client/scheme/openai_responses/capability.rs @@ -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, diff --git a/crates/llm-engine/src/llm_client/scheme/openai_responses/events.rs b/crates/llm-engine/src/llm_client/scheme/openai_responses/events.rs index 24ac3f26..47c8233b 100644 --- a/crates/llm-engine/src/llm_client/scheme/openai_responses/events.rs +++ b/crates/llm-engine/src/llm_client/scheme/openai_responses/events.rs @@ -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}; diff --git a/crates/llm-engine/src/llm_client/scheme/openai_responses/mod.rs b/crates/llm-engine/src/llm_client/scheme/openai_responses/mod.rs index 61256b5e..76e287a8 100644 --- a/crates/llm-engine/src/llm_client/scheme/openai_responses/mod.rs +++ b/crates/llm-engine/src/llm_client/scheme/openai_responses/mod.rs @@ -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, } diff --git a/crates/llm-engine/src/llm_client/scheme/openai_responses/request.rs b/crates/llm-engine/src/llm_client/scheme/openai_responses/request.rs index 291cc3be..d487601c 100644 --- a/crates/llm-engine/src/llm_client/scheme/openai_responses/request.rs +++ b/crates/llm-engine/src/llm_client/scheme/openai_responses/request.rs @@ -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, - /// 公式 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, #[serde(skip_serializing_if = "Option::is_none")] pub top_p: Option, - /// 会話単位の安定キー。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, } @@ -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, @@ -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); diff --git a/crates/llm-engine/src/llm_client/scheme/openai_responses/scheme_impl.rs b/crates/llm-engine/src/llm_client/scheme/openai_responses/scheme_impl.rs index e3f39a09..8a6e4e0a 100644 --- a/crates/llm-engine/src/llm_client/scheme/openai_responses/scheme_impl.rs +++ b/crates/llm-engine/src/llm_client/scheme/openai_responses/scheme_impl.rs @@ -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,27 +58,29 @@ impl Scheme for OpenAIResponsesScheme { fn validate_config(&self, config: &RequestConfig) -> Vec { 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)", + "OpenAI Responses compatible 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( "temperature", - "OpenAI Responses (ChatGPT backend)", + "OpenAI Responses compatible backend", )); } if config.top_p.is_some() { warnings.push(ConfigWarning::unsupported( "top_p", - "OpenAI Responses (ChatGPT backend)", + "OpenAI Responses compatible backend", )); } } diff --git a/crates/llm-engine/src/llm_client/transport.rs b/crates/llm-engine/src/llm_client/transport.rs index 63661332..f71edfc0 100644 --- a/crates/llm-engine/src/llm_client/transport.rs +++ b/crates/llm-engine/src/llm_client/transport.rs @@ -1,8 +1,8 @@ //! `HttpTransport`: すべての LLM wire scheme を共通の 1 本の //! HTTP クライアントで扱う。 //! -//! 旧 `providers/{anthropic,openai,gemini,ollama}.rs` を置き換える。 -//! scheme 固有の差分は [`Scheme`] trait 実装に委譲する。 +//! scheme 固有の差分は [`Scheme`] trait 実装に委譲し、backend 固有の +//! HTTP policy は [`TransportPolicy`] で明示的に差し込む。 use std::pin::Pin; use std::sync::Arc; @@ -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, @@ -63,6 +63,71 @@ impl ResolvedAuth { } } +/// Request body encoding policy used by [`HttpTransport`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RequestBodyEncoding { + /// Send the request body as plain JSON. + Json, + /// Send the request body as zstd-compressed JSON with `Content-Encoding: zstd`. + ZstdJson, +} + +impl Default for RequestBodyEncoding { + fn default() -> Self { + Self::Json + } +} + +/// Conversation header policy used by [`HttpTransport`]. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ConversationHeaderPolicy { + /// Do not derive any transport headers from [`Request::cache_key`]. + None, + /// Send OpenAI-compatible conversation headers from [`Request::cache_key`]. + OpenAiCompatible { + /// Send the legacy `session_id` header in addition to `session-id`. + include_legacy_session_id: bool, + /// Send `thread-id` with the same value as `session-id`. + include_thread_id: bool, + /// Send `x-client-request-id` with the same value as `session-id`. + include_client_request_id: bool, + }, +} + +impl Default for ConversationHeaderPolicy { + fn default() -> Self { + Self::None + } +} + +/// Backend-specific HTTP transport policy. +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct TransportPolicy { + /// How to serialize and encode the HTTP request body. + pub request_body_encoding: RequestBodyEncoding, + /// Optional backend-specific conversation headers derived from request metadata. + pub conversation_headers: ConversationHeaderPolicy, +} + +impl TransportPolicy { + /// Plain JSON requests with no derived conversation headers. + pub fn standard() -> Self { + Self::default() + } + + /// OpenAI-compatible backend profile that uses zstd JSON bodies and + /// conversation headers derived from [`Request::cache_key`]. + pub fn openai_compatible_zstd() -> Self { + Self { + request_body_encoding: RequestBodyEncoding::ZstdJson, + conversation_headers: ConversationHeaderPolicy::OpenAiCompatible { + include_legacy_session_id: true, + include_thread_id: true, + include_client_request_id: true, + }, + } + } +} fn header_value_for_diagnostics(headers: &HeaderMap, name: &str) -> Option { headers .get(name) @@ -120,6 +185,7 @@ pub struct HttpTransport { base_url: String, auth: ResolvedAuth, capability: ModelCapability, + policy: TransportPolicy, } impl HttpTransport { @@ -141,9 +207,16 @@ impl HttpTransport { base_url, auth, capability, + policy: TransportPolicy::default(), } } + /// Set a backend-specific HTTP transport policy. + pub fn with_transport_policy(mut self, policy: TransportPolicy) -> Self { + self.policy = policy; + self + } + /// カスタム HTTP クライアントを差し込む(テスト等)。 pub fn with_http_client(mut self, client: reqwest::Client) -> Self { self.http_client = client; @@ -206,13 +279,6 @@ impl HttpTransport { Ok(headers) } - fn is_codex_backend(&self) -> bool { - match &self.auth { - ResolvedAuth::Custom(provider) => provider.is_codex_backend(), - _ => false, - } - } - fn apply_stream_headers( &self, headers: &mut HeaderMap, @@ -220,19 +286,26 @@ impl HttpTransport { ) -> Result<(), ClientError> { headers.insert(ACCEPT, HeaderValue::from_static("text/event-stream")); - if self.is_codex_backend() + if let ConversationHeaderPolicy::OpenAiCompatible { + include_legacy_session_id, + include_thread_id, + include_client_request_id, + } = self.policy.conversation_headers && let Some(cache_key) = request.cache_key.as_deref() { let value = HeaderValue::from_str(cache_key).map_err(|e| { - ClientError::Config(format!("invalid Codex conversation header: {e}")) + ClientError::Config(format!("invalid conversation header value: {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. 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()); - headers.insert(HeaderName::from_static("x-client-request-id"), value); + if include_thread_id { + headers.insert(HeaderName::from_static("thread-id"), value.clone()); + } + if include_legacy_session_id { + headers.insert(HeaderName::from_static("session_id"), value.clone()); + } + if include_client_request_id { + headers.insert(HeaderName::from_static("x-client-request-id"), value); + } } Ok(()) @@ -243,19 +316,22 @@ impl HttpTransport { body: &serde_json::Value, headers: &mut HeaderMap, ) -> Result { - if !self.is_codex_backend() { - return Ok(RequestBody::Json(body.clone())); + match self.policy.request_body_encoding { + RequestBodyEncoding::Json => Ok(RequestBody::Json(body.clone())), + RequestBodyEncoding::ZstdJson => { + let raw = serde_json::to_vec(body)?; + let raw_json_bytes = raw.len(); + let compressed = + zstd::stream::encode_all(std::io::Cursor::new(raw), 3).map_err(|e| { + ClientError::Config(format!("failed to zstd-compress request: {e}")) + })?; + headers.insert(CONTENT_ENCODING, HeaderValue::from_static("zstd")); + Ok(RequestBody::CompressedJson { + bytes: compressed, + raw_json_bytes, + }) + } } - - let raw = serde_json::to_vec(body)?; - let raw_json_bytes = raw.len(); - let compressed = zstd::stream::encode_all(std::io::Cursor::new(raw), 3) - .map_err(|e| ClientError::Config(format!("failed to zstd-compress request: {e}")))?; - headers.insert(CONTENT_ENCODING, HeaderValue::from_static("zstd")); - Ok(RequestBody::CompressedJson { - bytes: compressed, - raw_json_bytes, - }) } } @@ -384,6 +460,7 @@ impl Clone for HttpTransport { base_url: self.base_url.clone(), auth: self.auth.clone(), capability: self.capability.clone(), + policy: self.policy.clone(), } } } @@ -447,7 +524,8 @@ impl LlmClient for HttpTransport { "path": path, "auth_kind": auth_kind(&self.auth), "required_auth": format!("{:?}", self.scheme.required_auth()), - "codex_backend": self.is_codex_backend(), + "request_body_encoding": format!("{:?}", self.policy.request_body_encoding), + "conversation_headers": format!("{:?}", self.policy.conversation_headers), "cache_key_present": request.cache_key.is_some(), "stream_open_timeout_ms": DEFAULT_STREAM_OPEN_TIMEOUT.as_millis() as u64, }), @@ -678,9 +756,7 @@ mod tests { use serde_json::json; #[derive(Debug)] - struct TestAuthProvider { - codex: bool, - } + struct TestAuthProvider; #[async_trait] impl AuthProvider for TestAuthProvider { @@ -696,10 +772,6 @@ mod tests { ), ]) } - - fn is_codex_backend(&self) -> bool { - self.codex - } } #[derive(Clone)] @@ -828,10 +900,9 @@ mod tests { } #[tokio::test] - async fn codex_backend_adds_conversation_headers_and_zstd_body() { - let transport = transport(ResolvedAuth::Custom(Arc::new(TestAuthProvider { - codex: true, - }))); + async fn transport_policy_adds_conversation_headers_and_zstd_body() { + let transport = transport(ResolvedAuth::Custom(Arc::new(TestAuthProvider))) + .with_transport_policy(TransportPolicy::openai_compatible_zstd()); let request = Request::new().user("hello").cache_key("segment-123"); let mut headers = transport.build_headers().await.unwrap(); transport @@ -870,7 +941,7 @@ mod tests { raw_json_bytes, } = encoded else { - panic!("Codex backend request body must be zstd-compressed"); + panic!("transport policy should zstd-compress request body"); }; assert!(raw_json_bytes > 0); let decoded = zstd::stream::decode_all(std::io::Cursor::new(compressed)).unwrap(); @@ -879,7 +950,7 @@ mod tests { } #[tokio::test] - async fn non_codex_request_does_not_get_codex_only_headers_or_compression() { + async fn standard_policy_does_not_get_conversation_headers_or_compression() { let transport = transport(ResolvedAuth::ApiKey("api-key".to_string())); let request = Request::new().user("hello").cache_key("segment-123"); let mut headers = transport.build_headers().await.unwrap(); @@ -901,7 +972,7 @@ mod tests { assert!(headers.get(CONTENT_ENCODING).is_none()); let RequestBody::Json(decoded) = encoded else { - panic!("non-Codex request body must remain normal JSON"); + panic!("standard transport policy should keep request body as JSON"); }; assert_eq!(decoded["prompt_cache_key"], "segment-123"); } diff --git a/crates/llm-engine/src/llm_client/types.rs b/crates/llm-engine/src/llm_client/types.rs index fbc62536..cd429441 100644 --- a/crates/llm-engine/src/llm_client/types.rs +++ b/crates/llm-engine/src/llm_client/types.rs @@ -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, /// 会話単位の安定キー。`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 は無視する。 diff --git a/crates/provider/src/codex_oauth/auth_json.rs b/crates/llm-engine/src/providers/codex/auth_json.rs similarity index 97% rename from crates/provider/src/codex_oauth/auth_json.rs rename to crates/llm-engine/src/providers/codex/auth_json.rs index 85e59a72..37422473 100644 --- a/crates/provider/src/codex_oauth/auth_json.rs +++ b/crates/llm-engine/src/providers/codex/auth_json.rs @@ -1,7 +1,7 @@ //! `~/.codex/auth.json` の読み書き。 //! -//! Codex CLI と schema を共有するが、yoi は知らないフィールドを -//! 失わないようファイル全体を `serde_json::Value` で保持し、必要箇所 +//! Codex CLI と schema を共有するが、知らないフィールドを失わないよう +//! ファイル全体を `serde_json::Value` で保持し、必要箇所 //! のみアクセスする。書込は `mode 0o600` を再設定(Codex CLI 同様)、 //! ファイルロックは取らない(manager 側で guarded reload)。 @@ -106,7 +106,7 @@ pub async fn load(path: &Path) -> Result { /// 既存ファイルを再読込し、`tokens.{id_token,access_token,refresh_token}` と /// `last_refresh` を更新して書き戻す。Codex CLI の `persist_tokens` 相当。 /// -/// 並行する Codex CLI / 別 yoi プロセスが先に refresh していた場合の +/// 並行する Codex CLI / 別プロセスが先に refresh していた場合の /// fields を保護するため、書込前に再 load して merge する。 pub async fn persist_refreshed( path: &Path, diff --git a/crates/provider/src/codex_oauth/error.rs b/crates/llm-engine/src/providers/codex/error.rs similarity index 98% rename from crates/provider/src/codex_oauth/error.rs rename to crates/llm-engine/src/providers/codex/error.rs index 06a7377a..7817a03d 100644 --- a/crates/provider/src/codex_oauth/error.rs +++ b/crates/llm-engine/src/providers/codex/error.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; -use llm_engine::llm_client::ClientError; +use crate::llm_client::ClientError; use thiserror::Error; #[derive(Debug, Error)] diff --git a/crates/provider/src/codex_oauth/jwt.rs b/crates/llm-engine/src/providers/codex/jwt.rs similarity index 100% rename from crates/provider/src/codex_oauth/jwt.rs rename to crates/llm-engine/src/providers/codex/jwt.rs diff --git a/crates/provider/src/codex_oauth/mod.rs b/crates/llm-engine/src/providers/codex/mod.rs similarity index 96% rename from crates/provider/src/codex_oauth/mod.rs rename to crates/llm-engine/src/providers/codex/mod.rs index 2df7b916..a3a5ad2f 100644 --- a/crates/provider/src/codex_oauth/mod.rs +++ b/crates/llm-engine/src/providers/codex/mod.rs @@ -4,11 +4,11 @@ //! //! 設計: //! -//! - llm-engine は [`AuthProvider`] trait しか知らず、実体である -//! [`CodexAuthProvider`] はこのクレートに置く(feedback_llm_engine_scope) +//! - HTTP transport は [`AuthProvider`] trait だけを見て、実体である +//! [`CodexAuthProvider`] はこの optional module に置く //! - access_token JWT の `exp` を読み、`now` 以下で proactive refresh //! (Codex CLI と同じバッファなし) -//! - 並行する Codex CLI / 別 yoi の refresh と取り違えないよう、 +//! - 並行する Codex CLI / 別プロセスの refresh と取り違えないよう、 //! refresh 直前に再 load して account_id 一致を確認(guarded reload) //! - ファイルロックは取らず、書込前に再 load + diff merge で吸収 //! - Codex の Keyring storage は対象外。auth.json 不在ならエラーで案内 @@ -21,9 +21,9 @@ mod refresh; use std::path::PathBuf; use std::sync::Arc; +use crate::llm_client::{ClientError, auth::AuthProvider}; use async_trait::async_trait; use chrono::{Duration, Utc}; -use llm_engine::llm_client::{ClientError, auth::AuthProvider}; use reqwest::header::{HeaderName, HeaderValue}; use tokio::sync::Mutex; @@ -152,7 +152,7 @@ impl CodexAuthProvider { .map_err(|e| CodexAuthError::InvalidHeader(format!("ChatGPT-Account-Id: {e}")))?; out.push((HeaderName::from_static("chatgpt-account-id"), acc_val)); - // Cloudflare WAF は ChatGPT backend アクセス元を `originator` / + // Cloudflare WAF は互換 backend アクセス元を `originator` / // `User-Agent` で識別する。Codex CLI が送る固定値を流用しないと // HTML challenge (403) を返されて SSE に到達できない。 out.push(( @@ -188,10 +188,6 @@ impl AuthProvider for CodexAuthProvider { .map_err(CodexAuthError::to_client_error)?; Self::build_headers(&snap).map_err(CodexAuthError::to_client_error) } - - fn is_codex_backend(&self) -> bool { - true - } } /// `access_token` の JWT `exp` を見て、期限切れなら true。 diff --git a/crates/provider/src/codex_oauth/refresh.rs b/crates/llm-engine/src/providers/codex/refresh.rs similarity index 98% rename from crates/provider/src/codex_oauth/refresh.rs rename to crates/llm-engine/src/providers/codex/refresh.rs index 1d94f8c9..1b526eeb 100644 --- a/crates/provider/src/codex_oauth/refresh.rs +++ b/crates/llm-engine/src/providers/codex/refresh.rs @@ -80,7 +80,7 @@ async fn response_with_timeout( .await .map_err(|_| { CodexAuthError::RefreshTransient(format!( - "codex_oauth_refresh timed out after {}s", + "oauth_token_refresh timed out after {}s", timeout.as_secs() )) })? diff --git a/crates/llm-engine/src/providers/mod.rs b/crates/llm-engine/src/providers/mod.rs new file mode 100644 index 00000000..868ff1ef --- /dev/null +++ b/crates/llm-engine/src/providers/mod.rs @@ -0,0 +1,4 @@ +//! Optional built-in provider/backend helpers. + +#[cfg(feature = "codex")] +pub mod codex; diff --git a/crates/llm-engine/src/prune.rs b/crates/llm-engine/src/prune.rs index 3ab45d89..4ad27b78 100644 --- a/crates/llm-engine/src/prune.rs +++ b/crates/llm-engine/src/prune.rs @@ -8,8 +8,8 @@ //! //! Prune は **コンテキスト射影** であり、history の変換ではない。 //! この crate が提供するのは pure な候補抽出 [`prunable_indices`] のみで、 -//! 射影の適用は上位層(`worker::prune_hook` 等)が LLM に送る一時コンテキスト -//! に対してだけ行う。Engine の永続履歴は決して変更されない。 +//! 射影の適用は LLM に送る一時コンテキストに対してだけ行う。 +//! Engine の永続履歴は決して変更されない。 //! //! 保護境界は末尾 token budget で決めるが、この crate は usage 履歴を //! 所有しない。prefix ごとの token 推定値と savings 推定は上位層から @@ -75,7 +75,7 @@ pub enum PruneDecision { } /// 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; /// Configuration for the Prune algorithm. diff --git a/crates/llm-engine/src/timeline/tool_call_collector.rs b/crates/llm-engine/src/timeline/tool_call_collector.rs index bce42971..fa04b0ed 100644 --- a/crates/llm-engine/src/timeline/tool_call_collector.rs +++ b/crates/llm-engine/src/timeline/tool_call_collector.rs @@ -130,13 +130,13 @@ mod tests { let mut timeline = Timeline::new(); timeline.on_tool_use_block(collector.clone()); - timeline.dispatch(&Event::tool_use_start(0, "tool_empty", "ListWorkers")); + timeline.dispatch(&Event::tool_use_start(0, "tool_empty", "ListItems")); timeline.dispatch(&Event::tool_use_stop(0)); let calls = collector.take_collected(); assert_eq!(calls.len(), 1); assert_eq!(calls[0].id, "tool_empty"); - assert_eq!(calls[0].name, "ListWorkers"); + assert_eq!(calls[0].name, "ListItems"); assert!(calls[0].input.is_object()); assert_eq!( calls[0].input, diff --git a/crates/llm-engine/src/tool_server.rs b/crates/llm-engine/src/tool_server.rs index c7e61963..21d373ac 100644 --- a/crates/llm-engine/src/tool_server.rs +++ b/crates/llm-engine/src/tool_server.rs @@ -75,10 +75,9 @@ impl ToolServerHandle { /// Execute all pending factories and register the resulting tools. /// /// Called implicitly by `Engine::lock()` before the first turn. - /// Exposed as `pub` so higher layers (e.g. Worker) can force-materialise - /// tools earlier — for example when building a system-prompt template - /// context that needs the list of registered tool names. Redundant - /// calls are no-ops. + /// Exposed as `pub` so callers can force-materialise tools earlier — + /// for example when building a system-prompt template context that needs + /// the list of registered tool names. Redundant calls are no-ops. /// /// # Panics /// diff --git a/crates/llm-engine/src/usage_record.rs b/crates/llm-engine/src/usage_record.rs index cf3f966b..948b8cf0 100644 --- a/crates/llm-engine/src/usage_record.rs +++ b/crates/llm-engine/src/usage_record.rs @@ -2,9 +2,8 @@ //! //! 1 リクエストの送信時点での「ある history prefix 長で計測した占有量」を //! 1 件分にまとめたもの。`UsageEvent` (provider stream イベント) を -//! 受けて呼び出し側 (typically Worker) が組み立て、永続化層 -//! (session-store) に流したり、token accounting (`token_counter`) で -//! 履歴として参照したりする。 +//! 受けて呼び出し側が組み立て、永続化層に流したり、token accounting +//! (`token_counter`) で履歴として参照したりする。 /// LLM リクエスト送信時点での占有量スナップショット。 #[derive(Debug, Clone, PartialEq, Eq)] diff --git a/crates/manifest/Cargo.toml b/crates/manifest/Cargo.toml index 9f51040a..a2a57e57 100644 --- a/crates/manifest/Cargo.toml +++ b/crates/manifest/Cargo.toml @@ -18,4 +18,5 @@ toml = { workspace = true } tracing = { workspace = true } [dev-dependencies] +serial_test = "3.4.0" tempfile = { workspace = true } diff --git a/crates/manifest/src/config.rs b/crates/manifest/src/config.rs index bae869b6..62692195 100644 --- a/crates/manifest/src/config.rs +++ b/crates/manifest/src/config.rs @@ -723,7 +723,7 @@ fn resolve_auth_file(auth: &mut Option, base: &Path) { /// モデル宣言に含まれる `auth.file` が絶対パスであることを検証する。 /// ref / scheme / model_id 等の論理的な有効性(ref があるか、inline が -/// 揃っているか)の検証はカタログを知る `crates/provider` 側で行う。 +/// 揃っているか)の検証は [`crate::model_catalog`] で行う。 fn validate_model_paths(model: &ModelManifest, field: &'static str) -> Result<(), ResolveError> { if let Some(AuthRef::ApiKey { file: Some(p), .. }) = &model.auth { ensure_absolute(field, p)?; diff --git a/crates/manifest/src/lib.rs b/crates/manifest/src/lib.rs index f2e57ce6..1f541eb9 100644 --- a/crates/manifest/src/lib.rs +++ b/crates/manifest/src/lib.rs @@ -1,6 +1,7 @@ mod config; pub mod defaults; mod model; +pub mod model_catalog; pub mod paths; pub mod plugin; mod profile; diff --git a/crates/manifest/src/model.rs b/crates/manifest/src/model.rs index 17d0e369..55da0c99 100644 --- a/crates/manifest/src/model.rs +++ b/crates/manifest/src/model.rs @@ -5,7 +5,7 @@ //! 直書き)の両方を受け入れるため、すべてのフィールドを `Option` として //! 持つ 1 つの型 [`ModelManifest`] に統合している。実解決(ref をプロバイダ //! カタログ / モデルカタログから引いて `scheme` や `model_id` を埋める) -//! は `crates/provider` の責務で、本モジュールはデータ表現のみを提供する。 +//! は [`crate::model_catalog`] の責務で、本モジュールはデータ表現のみを提供する。 //! //! 同じ型を partial(カスケード層)と完成形(最終マニフェスト)の両方で //! 使うことで、merge と最終変換の重複を避ける。 @@ -24,7 +24,7 @@ pub use llm_engine::llm_client::capability::{ModelCapability, ReasoningControl, /// - ref + 一部 override: ref で基底を引き、`auth` 等だけ書き換え /// - 完全 inline: `ref` を省略して `scheme` / `model_id` / `auth` を直書き /// -/// どの形が有効かの判定は `provider::resolve_model_manifest` が担う。 +/// どの形が有効かの判定は [`crate::model_catalog::resolve_model_manifest`] が担う。 /// 本クレートは「どこから取るか」を表現するだけで、未設定かどうかを /// 理由にした hard error は出さない。 #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] @@ -98,7 +98,7 @@ pub enum SchemeKind { /// 認証の参照。 /// /// 実際のトークン値の解決(local secret store / file 読取、OAuth refresh 等)は -/// `crates/provider` で行う。ここはあくまで「どこから取るか」の宣言。 +/// worker-side factory で行う。ここはあくまで「どこから取るか」の宣言。 #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] #[serde(tag = "kind", rename_all = "snake_case")] pub enum AuthRef { @@ -113,7 +113,7 @@ pub enum AuthRef { #[serde(default)] file: Option, }, - /// ChatGPT OAuth(`~/.codex/auth.json`)。実装は `llm-auth-codex-oauth` チケット + /// ChatGPT OAuth(`~/.codex/auth.json`)。 #[serde(rename = "codex_oauth")] CodexOAuth, /// Typed local secret-store reference. The profile resolver preserves this diff --git a/crates/provider/src/catalog.rs b/crates/manifest/src/model_catalog.rs similarity index 98% rename from crates/provider/src/catalog.rs rename to crates/manifest/src/model_catalog.rs index 47de0036..0cb9c5e2 100644 --- a/crates/provider/src/catalog.rs +++ b/crates/manifest/src/model_catalog.rs @@ -4,19 +4,19 @@ //! - builtin モデル: `resources/models/builtin.toml` //! - user override: `/{providers,models}.toml` //! -//! `` の解決は [`manifest::paths::config_dir`] を参照。 +//! `` の解決は [`crate::paths::config_dir`] を参照。 //! どちらの override も「あれば builtin を置換、無ければ builtin」と //! いう一方向の差し替え(マージしない)。providers / models は独立に //! 読み、片方だけ user override も可。 //! -//! [`resolve_model_manifest`] が `manifest::ModelManifest`(ref / inline +//! [`resolve_model_manifest`] が [`crate::ModelManifest`](ref / inline //! 両形)を最終的な [`ModelConfig`] に解決する単一の入口で、wire 層 //! に渡す前のバリデーションもここで行う。 use std::path::{Path, PathBuf}; +use crate::{AuthRef, ModelManifest, SchemeKind}; use llm_engine::llm_client::capability::ModelCapability; -use manifest::{AuthRef, ModelManifest, SchemeKind}; use serde::{Deserialize, Serialize}; const BUILTIN_PROVIDERS: &str = include_str!("../../../resources/providers/builtin.toml"); @@ -178,7 +178,7 @@ fn auth_hint_to_ref(hint: &AuthHint) -> AuthRef { /// ユーザーが書いた設定が silent に無視されて builtin に戻る挙動は /// 気付きにくいため)。 pub fn load_providers() -> Result, CatalogError> { - if let Some(path) = manifest::paths::user_catalog_override("providers.toml") + if let Some(path) = crate::paths::user_catalog_override("providers.toml") && path.is_file() { return load_providers_from(&path); @@ -211,7 +211,7 @@ pub fn load_providers_from(path: &Path) -> Result, CatalogErr /// builtin + user override を解決してモデルカタログを返す。 pub fn load_models() -> Result, CatalogError> { - if let Some(path) = manifest::paths::user_catalog_override("models.toml") + if let Some(path) = crate::paths::user_catalog_override("models.toml") && path.is_file() { return load_models_from(&path); diff --git a/crates/provider/Cargo.toml b/crates/provider/Cargo.toml deleted file mode 100644 index 562d8061..00000000 --- a/crates/provider/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "provider" -version = "0.1.0" -edition.workspace = true -license.workspace = true - -[dependencies] -async-trait = { workspace = true } -base64 = "0.22.1" -chrono = { version = "0.4", default-features = false, features = ["serde", "clock"] } -llm-engine = { workspace = true } -manifest = { workspace = true } -secrets = { workspace = true } -reqwest = { version = "0.13", features = ["json", "native-tls"] } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["sync", "fs", "rt", "time"] } -toml = { workspace = true } -tracing = { workspace = true } - -[dev-dependencies] -serial_test = "3.4.0" -tempfile = { workspace = true } -wiremock = "0.6.5" diff --git a/crates/provider/README.md b/crates/provider/README.md deleted file mode 100644 index e4aedc48..00000000 --- a/crates/provider/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# provider - -## Role - -`provider` resolves model/provider configuration and constructs provider-specific LLM clients for `llm-engine`. - -## Boundaries - -Owns: - -- builtin and user provider/model catalog resolution -- model reference expansion into concrete model config -- auth reference resolution through supported mechanisms -- provider/scheme capability and context-window metadata -- provider-specific client construction - -Does not own: - -- Engine turn lifecycle (`llm-engine`) -- secret storage internals (`secrets`) -- Worker lifecycle (`worker`) -- product CLI parsing (`yoi`) - -## Design notes - -Provider API facts drift. Keep wire-format, auth, catalog, and capability differences here so Engine semantics remain stable. - -Codex OAuth is a separate integration from normal provider secret refs because its local file shape and lifecycle differ. - -## See also - -- [`../../docs/design/provider-model-boundary.md`](../../docs/design/provider-model-boundary.md) -- [`../../docs/design/profiles-manifests-prompts.md`](../../docs/design/profiles-manifests-prompts.md) diff --git a/crates/tui/Cargo.toml b/crates/tui/Cargo.toml index 7af42fd1..bfd05343 100644 --- a/crates/tui/Cargo.toml +++ b/crates/tui/Cargo.toml @@ -23,7 +23,6 @@ manifest = { workspace = true } secrets = { workspace = true } session-store = { workspace = true } fs4 = { workspace = true } -provider = { workspace = true } ticket = { workspace = true } serde = { workspace = true, features = ["derive"] } minijinja = "2.19.0" diff --git a/crates/tui/src/setup_model.rs b/crates/tui/src/setup_model.rs index 00b5d4b9..43cb2e49 100644 --- a/crates/tui/src/setup_model.rs +++ b/crates/tui/src/setup_model.rs @@ -3,7 +3,7 @@ use std::io::{self, Write}; use std::path::{Path, PathBuf}; use std::process::ExitCode; -use provider::catalog::{self, AuthHint, ModelEntry, ProviderEntry}; +use manifest::model_catalog::{self as catalog, AuthHint, ModelEntry, ProviderEntry}; use toml::Value; use toml::map::Map; diff --git a/crates/worker/Cargo.toml b/crates/worker/Cargo.toml index 22a8e9b1..8dd5de43 100644 --- a/crates/worker/Cargo.toml +++ b/crates/worker/Cargo.toml @@ -11,12 +11,12 @@ default = [] [dependencies] async-trait = { workspace = true } clap = { version = "4.6.0", features = ["derive"] } -llm-engine = { workspace = true } +llm-engine = { workspace = true, features = ["codex"] } session-store = { workspace = true } +secrets = { workspace = true } manifest = { workspace = true } mcp = { workspace = true } protocol = { workspace = true } -provider = { workspace = true } client = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } @@ -46,6 +46,7 @@ futures-util = { version = "0.3", features = ["sink"] } [dev-dependencies] dotenv = "0.15.0" futures = { workspace = true } +serial_test = "3.4.0" tempfile = { workspace = true } wat = "1.241.2" yoi-plugin-pdk = { workspace = true } diff --git a/crates/worker/src/controller.rs b/crates/worker/src/controller.rs index 5452c00e..14ce16b0 100644 --- a/crates/worker/src/controller.rs +++ b/crates/worker/src/controller.rs @@ -1526,11 +1526,11 @@ where let manifest = worker.manifest(); // `build_client` がここに到達する前に同じマニフェストで成功している // ため、カタログ解決も必ず通る。念のため失敗時は "unknown" に落とす。 - let resolved = provider::catalog::resolve_model_manifest(&manifest.model).ok(); + let resolved = manifest::model_catalog::resolve_model_manifest(&manifest.model).ok(); let context_window = resolved .as_ref() .map(|cfg| cfg.context_window) - .unwrap_or(provider::catalog::DEFAULT_CONTEXT_WINDOW); + .unwrap_or(manifest::model_catalog::DEFAULT_CONTEXT_WINDOW); let (provider_name, model_id) = match resolved { Some(cfg) => { let name = match cfg.scheme { diff --git a/crates/worker/src/lib.rs b/crates/worker/src/lib.rs index ed45f169..e265b6b6 100644 --- a/crates/worker/src/lib.rs +++ b/crates/worker/src/lib.rs @@ -8,6 +8,7 @@ pub mod fs_view; pub mod hook; pub(crate) mod in_flight; pub mod ipc; +pub mod model_client; pub mod prompt; pub mod runtime; pub mod segment_log_sink; @@ -30,11 +31,11 @@ pub use manifest::{ AuthRef, ModelManifest, SchemeKind, Scope, WorkerManifest, WorkerManifestConfig, WorkerMetaConfig, }; +pub use model_client::{ProviderError, build_client}; pub use prompt::catalog::{CatalogError, PromptCatalog, WorkerPrompt}; pub use prompt::loader::PromptLoader; pub use prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTemplate}; pub use protocol::{ErrorCode, Event, Method, TurnResult, WorkerStatus}; -pub use provider::{ProviderError, build_client}; pub use runtime::dir::RuntimeDir; pub use segment_log_sink::SegmentLogSink; pub use shared_state::WorkerSharedState; diff --git a/crates/provider/src/lib.rs b/crates/worker/src/model_client.rs similarity index 82% rename from crates/provider/src/lib.rs rename to crates/worker/src/model_client.rs index 2962a308..6574ec1a 100644 --- a/crates/provider/src/lib.rs +++ b/crates/worker/src/model_client.rs @@ -1,5 +1,4 @@ -//! Worker マニフェストの [`ModelManifest`] を [`Box`] -//! に落とすファクトリ。 +//! [`ModelManifest`] を [`Box`] に落とす worker-side factory。 //! //! 段階: //! 1. `ModelManifest` を [`catalog::resolve_model_manifest`] で @@ -11,11 +10,7 @@ //! `catalog::resolve_model_manifest` が [`ModelConfig`] に詰め込む) //! //! llm-engine は低レベル基盤に留める方針なので、高レベル側で必要に -//! なる認証ストア解決(Codex OAuth の `~/.codex/auth.json` 読取等)は -//! このクレートに追加する。 - -pub mod catalog; -pub mod codex_oauth; +//! なる認証ストア解決と secret store 解決は worker 側で行う。 use std::sync::Arc; @@ -26,13 +21,14 @@ use llm_engine::llm_client::{ Scheme, anthropic::AnthropicScheme, gemini::GeminiScheme, openai_chat::OpenAIScheme, openai_responses::OpenAIResponsesScheme, }, - transport::{HttpTransport, ResolvedAuth}, + transport::{HttpTransport, ResolvedAuth, TransportPolicy}, }; +use llm_engine::providers::codex::CodexAuthProvider; -use manifest::{AuthRef, ModelManifest, SchemeKind}; +use manifest::{AuthRef, ModelManifest, SchemeKind, model_catalog as catalog}; use secrets::{SecretStore, SecretValue}; -pub use catalog::{ModelConfig, ResolveError as CatalogResolveError}; +pub use manifest::model_catalog::{ModelConfig, ResolveError as CatalogResolveError}; /// プロバイダ構築時のエラー。 #[derive(Debug, thiserror::Error)] @@ -111,7 +107,7 @@ fn resolve_auth_with_resolver( Err(ProviderError::ApiKeyMissing { scheme }) } AuthRef::CodexOAuth => { - let provider = codex_oauth::CodexAuthProvider::from_default_home() + let provider = CodexAuthProvider::from_default_home() .map_err(|e| ProviderError::Config(e.to_string()))?; Ok(ResolvedAuth::Custom(Arc::new(provider))) } @@ -144,6 +140,7 @@ fn build_transport( scheme: S, config: &ModelConfig, resolved: ResolvedAuth, + policy: TransportPolicy, ) -> Result, ProviderError> { if !resolved.matches(scheme.required_auth()) { return Err(ProviderError::AuthMismatch { @@ -160,21 +157,39 @@ fn build_transport( .clone() .unwrap_or_else(|| scheme.default_capability()); let base_url = effective_base_url(&scheme, config); - Ok(Box::new(HttpTransport::new( - scheme, - config.model_id.clone(), - base_url, - resolved, - capability, - ))) + Ok(Box::new( + HttpTransport::new( + scheme, + config.model_id.clone(), + base_url, + resolved, + capability, + ) + .with_transport_policy(policy), + )) } fn build_from_config(config: &ModelConfig) -> Result, ProviderError> { let resolved = resolve_auth(config.scheme, &config.auth)?; match config.scheme { - SchemeKind::Anthropic => build_transport(AnthropicScheme::new(), config, resolved), - SchemeKind::OpenaiChat => build_transport(OpenAIScheme::new(), config, resolved), - SchemeKind::Gemini => build_transport(GeminiScheme::new(), config, resolved), + SchemeKind::Anthropic => build_transport( + AnthropicScheme::new(), + config, + resolved, + TransportPolicy::standard(), + ), + SchemeKind::OpenaiChat => build_transport( + OpenAIScheme::new(), + config, + resolved, + TransportPolicy::standard(), + ), + SchemeKind::Gemini => build_transport( + GeminiScheme::new(), + config, + resolved, + TransportPolicy::standard(), + ), SchemeKind::OpenaiResponses => { // ChatGPT backend (codex-oauth) は `max_output_tokens` / // `temperature` / `top_p` を 400 で弾くため、その経路では @@ -183,7 +198,12 @@ fn build_from_config(config: &ModelConfig) -> Result, Provide let scheme = OpenAIResponsesScheme::new() .with_send_max_output_tokens(send_to_official) .with_send_sampling_params(send_to_official); - build_transport(scheme, config, resolved) + let policy = if matches!(config.auth, AuthRef::CodexOAuth) { + TransportPolicy::openai_compatible_zstd() + } else { + TransportPolicy::standard() + }; + build_transport(scheme, config, resolved, policy) } } } @@ -208,10 +228,34 @@ mod tests { use super::*; use serial_test::serial; use std::io::Write; - use std::path::PathBuf; + use std::path::{Path, PathBuf}; struct TestSecrets(std::collections::BTreeMap); + struct ConfigDirGuard { + prev: Option, + } + + impl ConfigDirGuard { + fn new(path: &Path) -> Self { + let prev = std::env::var("YOI_CONFIG_DIR").ok(); + // SAFETY: tests using this guard are marked `#[serial]`. + unsafe { std::env::set_var("YOI_CONFIG_DIR", path) }; + Self { prev } + } + } + + impl Drop for ConfigDirGuard { + fn drop(&mut self) { + unsafe { + match &self.prev { + Some(v) => std::env::set_var("YOI_CONFIG_DIR", v), + None => std::env::remove_var("YOI_CONFIG_DIR"), + } + } + } + } + impl SecretResolver for TestSecrets { fn get_secret(&self, id: &str) -> Result { self.0 @@ -311,7 +355,11 @@ mod tests { } #[test] + #[serial] fn ref_manifest_builds_client() { + let dir = tempfile::tempdir().unwrap(); + let _guard = ConfigDirGuard::new(dir.path()); + // Ollama は AuthRef::None で構築できる end-to-end path。 let manifest = ModelManifest { ref_: Some("ollama-local/llama3.1".into()), @@ -326,7 +374,11 @@ mod tests { } #[test] + #[serial] fn inline_manifest_builds_client() { + let dir = tempfile::tempdir().unwrap(); + let _guard = ConfigDirGuard::new(dir.path()); + // Form C: 完全直書き。Ollama 相当を AuthRef::None で構築。 let manifest = ModelManifest { scheme: Some(SchemeKind::Anthropic), diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index 35148b8b..098aa5fd 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -3132,7 +3132,7 @@ impl Worker { fn build_compactor_client(&self) -> Result, WorkerError> { if let Some(ref compaction) = self.manifest.compaction { if let Some(ref model_config) = compaction.model { - let client = provider::build_client(model_config)?; + let client = crate::model_client::build_client(model_config)?; return Ok(client); } } @@ -3149,7 +3149,7 @@ impl Worker { memory_cfg: &manifest::MemoryConfig, ) -> Result, WorkerError> { if let Some(ref m) = memory_cfg.extract_model { - let client = provider::build_client(m)?; + let client = crate::model_client::build_client(m)?; return Ok(client); } let worker = self.engine.as_ref().expect("worker taken during run"); @@ -3586,7 +3586,7 @@ impl Worker { memory_cfg: &manifest::MemoryConfig, ) -> Result, WorkerError> { if let Some(ref m) = memory_cfg.consolidation_model { - let client = provider::build_client(m)?; + let client = crate::model_client::build_client(m)?; return Ok(client); } let worker = self.engine.as_ref().expect("worker taken during run"); @@ -5154,7 +5154,7 @@ pub enum WorkerError { ManifestResolve(#[source] ResolveError), #[error(transparent)] - Provider(#[from] provider::ProviderError), + Provider(#[from] crate::model_client::ProviderError), #[error("compaction thrash: context still exceeds threshold immediately after compact")] CompactThrash, @@ -5383,7 +5383,7 @@ fn prepare_worker_common_from_scope( let delegation_scope = DelegationScope::from_config(&manifest.delegation_scope).map_err(WorkerError::Scope)?; - let client = provider::build_client(&manifest.model)?; + let client = crate::model_client::build_client(&manifest.model)?; let prompts = PromptCatalog::load(loader, manifest.worker.prompt_pack.as_deref())?; let memory_layout = manifest.memory.as_ref().and_then(|mem| { filesystem_authority diff --git a/docs/manifest.toml b/docs/manifest.toml index 8fcd9204..1d542488 100644 --- a/docs/manifest.toml +++ b/docs/manifest.toml @@ -43,7 +43,7 @@ name = "example-agent" # (b) `ref` + 一部 override — auth など個別差し替え # (c) `scheme` + `model_id` 直書き — カタログを使わない inline 指定 # (b) / (c) では `ref` 未指定なら `scheme` / `model_id` / `auth` が必須。 -# (実際の必須判定は `crates/provider` の resolve 側で行う) +# (実際の必須判定は manifest::model_catalog の resolve 側で行う) [model] # 任意。形式: "/"。