From 839fc7b40c2191320057f4debf24e2870052c771 Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 15 Jul 2026 00:58:29 +0900 Subject: [PATCH] refactor: fold provider crate into engine --- Cargo.lock | 103 ++---------- Cargo.toml | 3 - crates/llm-engine/Cargo.toml | 8 +- crates/llm-engine/src/lib.rs | 1 + crates/llm-engine/src/llm_client/auth.rs | 8 - crates/llm-engine/src/llm_client/retry.rs | 8 +- .../llm_client/scheme/anthropic/capability.rs | 4 +- .../llm_client/scheme/gemini/capability.rs | 2 +- .../scheme/openai_chat/capability.rs | 4 +- .../scheme/openai_responses/scheme_impl.rs | 6 +- crates/llm-engine/src/llm_client/transport.rs | 158 +++++++++++++----- .../src/providers/codex}/auth_json.rs | 6 +- .../src/providers/codex}/error.rs | 2 +- .../src/providers/codex}/jwt.rs | 0 .../src/providers/codex}/mod.rs | 14 +- .../src/providers/codex}/refresh.rs | 2 +- crates/llm-engine/src/providers/mod.rs | 4 + .../src/timeline/tool_call_collector.rs | 4 +- crates/manifest/Cargo.toml | 1 + crates/manifest/src/config.rs | 2 +- crates/manifest/src/lib.rs | 1 + crates/manifest/src/model.rs | 8 +- .../src/model_catalog.rs} | 10 +- crates/provider/Cargo.toml | 25 --- crates/provider/README.md | 33 ---- crates/tui/Cargo.toml | 1 - crates/tui/src/setup_model.rs | 2 +- crates/worker/Cargo.toml | 5 +- crates/worker/src/controller.rs | 4 +- crates/worker/src/lib.rs | 3 +- .../src/lib.rs => worker/src/model_client.rs} | 98 ++++++++--- crates/worker/src/worker.rs | 10 +- docs/manifest.toml | 2 +- 33 files changed, 263 insertions(+), 279 deletions(-) rename crates/{provider/src/codex_oauth => llm-engine/src/providers/codex}/auth_json.rs (97%) rename crates/{provider/src/codex_oauth => llm-engine/src/providers/codex}/error.rs (98%) rename crates/{provider/src/codex_oauth => llm-engine/src/providers/codex}/jwt.rs (100%) rename crates/{provider/src/codex_oauth => llm-engine/src/providers/codex}/mod.rs (96%) rename crates/{provider/src/codex_oauth => llm-engine/src/providers/codex}/refresh.rs (98%) create mode 100644 crates/llm-engine/src/providers/mod.rs rename crates/{provider/src/catalog.rs => manifest/src/model_catalog.rs} (98%) delete mode 100644 crates/provider/Cargo.toml delete mode 100644 crates/provider/README.md rename crates/{provider/src/lib.rs => worker/src/model_client.rs} (82%) 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/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/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 e5e061f9..e35f4fff 100644 --- a/crates/llm-engine/src/llm_client/auth.rs +++ b/crates/llm-engine/src/llm_client/auth.rs @@ -40,12 +40,4 @@ pub enum AuthRequirement { pub trait AuthProvider: Send + Sync + std::fmt::Debug { /// 1 リクエスト分の認証ヘッダを返す。refresh が必要なら内部で行う。 async fn headers(&self) -> Result, ClientError>; - - /// Conversation header / request compression が必要な backend profile かどうか。 - /// - /// transport は呼び出し側の具象型を知らないため、この hook だけで - /// 追加の wire behavior を切り替える。 - 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/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/scheme_impl.rs b/crates/llm-engine/src/llm_client/scheme/openai_responses/scheme_impl.rs index 7c08c130..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 @@ -65,7 +65,7 @@ impl Scheme for OpenAIResponsesScheme { 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", )); } // Same for `temperature` / `top_p` on compatible backends that @@ -74,13 +74,13 @@ impl Scheme for OpenAIResponsesScheme { 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 a1272099..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; @@ -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,18 +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}")) })?; - // 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()); - 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(()) @@ -242,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, - }) } } @@ -383,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(), } } } @@ -446,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, }), @@ -677,9 +756,7 @@ mod tests { use serde_json::json; #[derive(Debug)] - struct TestAuthProvider { - codex: bool, - } + struct TestAuthProvider; #[async_trait] impl AuthProvider for TestAuthProvider { @@ -695,10 +772,6 @@ mod tests { ), ]) } - - fn is_codex_backend(&self) -> bool { - self.codex - } } #[derive(Clone)] @@ -827,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 @@ -869,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(); @@ -878,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(); @@ -900,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/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/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/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 b25ddb46..d5fdcb38 100644 --- a/crates/worker/src/controller.rs +++ b/crates/worker/src/controller.rs @@ -1449,11 +1449,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 8ad93cb5..7c3c8ef5 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -3048,7 +3048,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); } } @@ -3065,7 +3065,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"); @@ -3502,7 +3502,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"); @@ -5070,7 +5070,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, @@ -5299,7 +5299,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] # 任意。形式: "/"。