merge: prepare llm engine for publish
This commit is contained in:
commit
88c2edefd4
103
Cargo.lock
generated
103
Cargo.lock
generated
|
|
@ -548,16 +548,6 @@ dependencies = [
|
||||||
"unicode-segmentation",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.10.1"
|
version = "0.10.1"
|
||||||
|
|
@ -1668,11 +1658,9 @@ dependencies = [
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2",
|
"socket2",
|
||||||
"system-configuration",
|
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-registry",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -2108,6 +2096,8 @@ name = "llm-engine"
|
||||||
version = "0.2.1"
|
version = "0.2.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-trait",
|
"async-trait",
|
||||||
|
"base64",
|
||||||
|
"chrono",
|
||||||
"clap",
|
"clap",
|
||||||
"dotenv",
|
"dotenv",
|
||||||
"eventsource-stream",
|
"eventsource-stream",
|
||||||
|
|
@ -2231,6 +2221,7 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_ignored",
|
"serde_ignored",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serial_test",
|
||||||
"sha2 0.10.9",
|
"sha2 0.10.9",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
|
|
@ -2907,28 +2898,6 @@ dependencies = [
|
||||||
"uuid",
|
"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]]
|
[[package]]
|
||||||
name = "pulldown-cmark"
|
name = "pulldown-cmark"
|
||||||
version = "0.13.3"
|
version = "0.13.3"
|
||||||
|
|
@ -3291,7 +3260,6 @@ checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
"bytes",
|
"bytes",
|
||||||
"encoding_rs",
|
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
@ -3305,7 +3273,6 @@ dependencies = [
|
||||||
"hyper-util",
|
"hyper-util",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"mime",
|
|
||||||
"native-tls",
|
"native-tls",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
|
@ -3447,7 +3414,7 @@ version = "0.6.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
|
checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"core-foundation 0.10.1",
|
"core-foundation",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"jni",
|
"jni",
|
||||||
"log",
|
"log",
|
||||||
|
|
@ -3501,15 +3468,6 @@ dependencies = [
|
||||||
"winapi-util",
|
"winapi-util",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "scc"
|
|
||||||
version = "2.4.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "46e6f046b7fef48e2660c57ed794263155d713de679057f2d0c169bfc6e756cc"
|
|
||||||
dependencies = [
|
|
||||||
"sdd",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "schannel"
|
name = "schannel"
|
||||||
version = "0.1.29"
|
version = "0.1.29"
|
||||||
|
|
@ -3550,12 +3508,6 @@ version = "1.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sdd"
|
|
||||||
version = "3.0.10"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "490dcfcbfef26be6800d11870ff2df8774fa6e86d047e3e8c8a76b25655e41ca"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "secrets"
|
name = "secrets"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -3574,7 +3526,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.0",
|
"bitflags 2.11.0",
|
||||||
"core-foundation 0.10.1",
|
"core-foundation",
|
||||||
"core-foundation-sys",
|
"core-foundation-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"security-framework-sys",
|
"security-framework-sys",
|
||||||
|
|
@ -3711,24 +3663,23 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serial_test"
|
name = "serial_test"
|
||||||
version = "3.4.0"
|
version = "3.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "911bd979bf1070a3f3aa7b691a3b3e9968f339ceeec89e08c280a8a22207a32f"
|
checksum = "699f4197115b8a7e7ff19c9a315a4bd6fffec26cc4626ef45ecaea389e081c6d"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-executor",
|
"futures-executor",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"scc",
|
|
||||||
"serial_test_derive",
|
"serial_test_derive",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serial_test_derive"
|
name = "serial_test_derive"
|
||||||
version = "3.4.0"
|
version = "3.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0a7d91949b85b0d2fb687445e448b40d322b6b3e4af6b44a29b21d9a5f33e6d9"
|
checksum = "94e153fc76e1c6a068703d6d29c508a0b15c061c4b7e43da59cc097bc342673c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
|
@ -4016,27 +3967,6 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"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]]
|
[[package]]
|
||||||
name = "tar"
|
name = "tar"
|
||||||
version = "0.4.46"
|
version = "0.4.46"
|
||||||
|
|
@ -4602,7 +4532,6 @@ dependencies = [
|
||||||
"manifest",
|
"manifest",
|
||||||
"minijinja",
|
"minijinja",
|
||||||
"protocol",
|
"protocol",
|
||||||
"provider",
|
|
||||||
"pulldown-cmark",
|
"pulldown-cmark",
|
||||||
"ratatui",
|
"ratatui",
|
||||||
"secrets",
|
"secrets",
|
||||||
|
|
@ -5460,17 +5389,6 @@ version = "0.2.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
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]]
|
[[package]]
|
||||||
name = "windows-result"
|
name = "windows-result"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
|
|
@ -5804,11 +5722,12 @@ dependencies = [
|
||||||
"memory",
|
"memory",
|
||||||
"minijinja",
|
"minijinja",
|
||||||
"protocol",
|
"protocol",
|
||||||
"provider",
|
|
||||||
"reqwest",
|
"reqwest",
|
||||||
"schemars",
|
"schemars",
|
||||||
|
"secrets",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"serial_test",
|
||||||
"session-metrics",
|
"session-metrics",
|
||||||
"session-store",
|
"session-store",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ members = [
|
||||||
"crates/plugin-pdk",
|
"crates/plugin-pdk",
|
||||||
"crates/yoi",
|
"crates/yoi",
|
||||||
"crates/protocol",
|
"crates/protocol",
|
||||||
"crates/provider",
|
|
||||||
"crates/session-metrics",
|
"crates/session-metrics",
|
||||||
"crates/session-analytics",
|
"crates/session-analytics",
|
||||||
"crates/lint-common",
|
"crates/lint-common",
|
||||||
|
|
@ -39,7 +38,6 @@ default-members = [
|
||||||
"crates/plugin-pdk",
|
"crates/plugin-pdk",
|
||||||
"crates/yoi",
|
"crates/yoi",
|
||||||
"crates/protocol",
|
"crates/protocol",
|
||||||
"crates/provider",
|
|
||||||
"crates/session-metrics",
|
"crates/session-metrics",
|
||||||
"crates/session-analytics",
|
"crates/session-analytics",
|
||||||
"crates/lint-common",
|
"crates/lint-common",
|
||||||
|
|
@ -72,7 +70,6 @@ worker-runtime = { path = "crates/worker-runtime" }
|
||||||
yoi-plugin-pdk = { path = "crates/plugin-pdk" }
|
yoi-plugin-pdk = { path = "crates/plugin-pdk" }
|
||||||
yoi = { path = "crates/yoi" }
|
yoi = { path = "crates/yoi" }
|
||||||
protocol = { path = "crates/protocol" }
|
protocol = { path = "crates/protocol" }
|
||||||
provider = { path = "crates/provider" }
|
|
||||||
session-metrics = { path = "crates/session-metrics" }
|
session-metrics = { path = "crates/session-metrics" }
|
||||||
session-analytics = { path = "crates/session-analytics" }
|
session-analytics = { path = "crates/session-analytics" }
|
||||||
session-store = { path = "crates/session-store" }
|
session-store = { path = "crates/session-store" }
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ Does not own:
|
||||||
|
|
||||||
## Design notes
|
## Design notes
|
||||||
|
|
||||||
Macros reduce boilerplate, but they must not imply capability. A generated tool definition is still subject to manifest permissions, Worker scope, and runtime policy.
|
Macros reduce boilerplate, but they must not imply capability. A generated tool definition is still subject to host permissions, application scope, and runtime policy.
|
||||||
|
|
||||||
## See also
|
## See also
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@ version = "0.2.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
codex = ["dep:base64", "dep:chrono"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|
@ -12,11 +16,13 @@ thiserror = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
futures = { 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"
|
tokio-util = "0.7"
|
||||||
reqwest = { version = "0.13", default-features = false, features = ["stream", "json", "native-tls", "http2"] }
|
reqwest = { version = "0.13", default-features = false, features = ["stream", "json", "native-tls", "http2"] }
|
||||||
eventsource-stream = "0.2"
|
eventsource-stream = "0.2"
|
||||||
zstd = "0.13"
|
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 }
|
llm-engine-macros = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,10 @@ Owns:
|
||||||
|
|
||||||
Does not own:
|
Does not own:
|
||||||
|
|
||||||
- Worker names, sockets, process lifecycle, or scope delegation (`worker`)
|
- Host application names, sockets, process lifecycle, or scope delegation
|
||||||
- product CLI shape (`yoi`)
|
- Product CLI shape
|
||||||
- provider catalog and secret resolution (`provider`, `secrets`)
|
- Provider catalog and secret resolution
|
||||||
- durable Worker current state (`session-store` worker metadata)
|
- Durable application state outside engine history
|
||||||
|
|
||||||
## Design notes
|
## Design notes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -213,9 +213,9 @@ pub struct Engine<C: LlmClient, S: EngineState = Mutable> {
|
||||||
/// stream events become visible.
|
/// stream events become visible.
|
||||||
lifecycle_trace_cbs: Vec<Arc<dyn Fn(usize, usize, &str, &Value) + Send + Sync>>,
|
lifecycle_trace_cbs: Vec<Arc<dyn Fn(usize, usize, &str, &Value) + Send + Sync>>,
|
||||||
/// Non-fatal warning callbacks. Invoked when the Engine wants to
|
/// Non-fatal warning callbacks. Invoked when the Engine wants to
|
||||||
/// surface an advisory message to the upper layer (e.g. Worker) so it
|
/// surface an advisory message to the caller so it can be forwarded
|
||||||
/// can be forwarded to the user — distinct from `tracing::warn!`,
|
/// to the user — distinct from `tracing::warn!`, which is for
|
||||||
/// which is for developer-facing logs.
|
/// developer-facing logs.
|
||||||
warning_cbs: Vec<Box<dyn Fn(&str) + Send + Sync>>,
|
warning_cbs: Vec<Box<dyn Fn(&str) + Send + Sync>>,
|
||||||
/// Tool-result callbacks. Invoked once per completed tool call
|
/// Tool-result callbacks. Invoked once per completed tool call
|
||||||
/// after post-execution interceptors and the output byte-cap
|
/// after post-execution interceptors and the output byte-cap
|
||||||
|
|
@ -253,8 +253,8 @@ pub struct Engine<C: LlmClient, S: EngineState = Mutable> {
|
||||||
/// Plumbed into [`Request::cache_anchor`] at request build time.
|
/// Plumbed into [`Request::cache_anchor`] at request build time.
|
||||||
cache_anchor: Option<usize>,
|
cache_anchor: Option<usize>,
|
||||||
/// Conversation-scoped cache key, set by higher layers. Plumbed into
|
/// Conversation-scoped cache key, set by higher layers. Plumbed into
|
||||||
/// [`Request::cache_key`] at request build time. Worker 側では
|
/// [`Request::cache_key`] at request build time. Callers should pass a
|
||||||
/// `SegmentId` を渡す。
|
/// stable conversation identifier when the backend benefits from one.
|
||||||
cache_key: Option<String>,
|
cache_key: Option<String>,
|
||||||
/// State marker
|
/// State marker
|
||||||
_state: PhantomData<S>,
|
_state: PhantomData<S>,
|
||||||
|
|
@ -486,8 +486,8 @@ impl<C: LlmClient, S: EngineState> Engine<C, S> {
|
||||||
///
|
///
|
||||||
/// Fired after `post_tool_call` interceptors and any `content`
|
/// Fired after `post_tool_call` interceptors and any `content`
|
||||||
/// truncation from `tool_output_limits`, so the callback observes
|
/// truncation from `tool_output_limits`, so the callback observes
|
||||||
/// exactly what is persisted to history. Intended for upper layers
|
/// exactly what is persisted to history. Intended for callers that need
|
||||||
/// (e.g. Worker) to forward tool results to clients.
|
/// to forward tool results to clients.
|
||||||
pub fn on_tool_result(&mut self, callback: impl Fn(&ToolResult) + Send + Sync + 'static) {
|
pub fn on_tool_result(&mut self, callback: impl Fn(&ToolResult) + Send + Sync + 'static) {
|
||||||
self.tool_result_cbs.push(Box::new(callback));
|
self.tool_result_cbs.push(Box::new(callback));
|
||||||
}
|
}
|
||||||
|
|
@ -1121,10 +1121,10 @@ impl<C: LlmClient, S: EngineState> Engine<C, S> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Drain interceptor-side inputs that are meant to land in
|
// Drain interceptor-side inputs that are meant to land in
|
||||||
// history (notifications, cross-Worker events, system
|
// history (notifications, external events, system reminders).
|
||||||
// reminders). These are committed *before* the per-request
|
// These are committed *before* the per-request clone so they
|
||||||
// clone so they participate in the LLM request below and
|
// participate in the LLM request below and get persisted by
|
||||||
// get persisted by the upper layer that owns history.json.
|
// the caller that owns durable history.
|
||||||
let pending = self.interceptor.pending_history_appends().await;
|
let pending = self.interceptor.pending_history_appends().await;
|
||||||
if !pending.is_empty() {
|
if !pending.is_empty() {
|
||||||
self.append_history_items(pending);
|
self.append_history_items(pending);
|
||||||
|
|
@ -1301,9 +1301,7 @@ impl<C: LlmClient, S: EngineState> Engine<C, S> {
|
||||||
self.turn_count += 1;
|
self.turn_count += 1;
|
||||||
|
|
||||||
// Collect and commit assistant items. Routed through
|
// Collect and commit assistant items. Routed through
|
||||||
// `append_history_items` so observers (e.g. the
|
// `append_history_items` so observers see each item as it lands.
|
||||||
// Worker-side per-item session-log committer) see each item
|
|
||||||
// as it lands.
|
|
||||||
let reasoning_items = self.thinking_block_collector.take_collected();
|
let reasoning_items = self.thinking_block_collector.take_collected();
|
||||||
let text_blocks = self.text_block_collector.take_collected();
|
let text_blocks = self.text_block_collector.take_collected();
|
||||||
let tool_calls = self.tool_call_collector.take_collected();
|
let tool_calls = self.tool_call_collector.take_collected();
|
||||||
|
|
@ -1603,8 +1601,7 @@ impl<C: LlmClient, S: EngineState> Engine<C, S> {
|
||||||
}
|
}
|
||||||
Ok(ToolExecutionResult::Completed(results)) => {
|
Ok(ToolExecutionResult::Completed(results)) => {
|
||||||
// Route per-result pushes through the callback path so
|
// Route per-result pushes through the callback path so
|
||||||
// observers (e.g. the Worker-side per-item session-log
|
// observers see each tool result as it lands.
|
||||||
// committer) see each tool result as it lands.
|
|
||||||
let items = results.into_iter().map(|result| {
|
let items = results.into_iter().map(|result| {
|
||||||
Item::tool_result_item(
|
Item::tool_result_item(
|
||||||
&result.tool_use_id,
|
&result.tool_use_id,
|
||||||
|
|
@ -1707,9 +1704,9 @@ impl<C: LlmClient> Engine<C, Mutable> {
|
||||||
|
|
||||||
/// Install byte-size caps for tool execution `content`.
|
/// Install byte-size caps for tool execution `content`.
|
||||||
///
|
///
|
||||||
/// Passing `None` (the default) disables truncation. Higher layers
|
/// Passing `None` (the default) disables truncation. Callers translate
|
||||||
/// (e.g. Worker) translate manifest configuration into a concrete
|
/// their own configuration into a concrete [`ToolOutputLimits`] and
|
||||||
/// [`ToolOutputLimits`] and install it here.
|
/// install it here.
|
||||||
pub fn set_tool_output_limits(&mut self, limits: Option<ToolOutputLimits>) {
|
pub fn set_tool_output_limits(&mut self, limits: Option<ToolOutputLimits>) {
|
||||||
self.tool_output_limits = limits;
|
self.tool_output_limits = limits;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
//! Interceptor - control flow delegation for the Engine execution loop
|
//! Interceptor - control flow delegation for the Engine execution loop
|
||||||
//!
|
//!
|
||||||
//! Defines the [`Interceptor`] trait that upper layers (e.g. Worker) implement
|
//! Defines the [`Interceptor`] trait that callers implement to inject
|
||||||
//! to inject orchestration decisions (approval, skip, pause, abort)
|
//! orchestration decisions (approval, skip, pause, abort) into the Engine's
|
||||||
//! into the Engine's turn loop without the Engine knowing about
|
//! turn loop without the Engine knowing about host-application concepts.
|
||||||
//! higher-level concepts.
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -132,8 +131,8 @@ pub struct ToolResultInfo {
|
||||||
/// Intercepts the Engine execution loop at key decision points.
|
/// Intercepts the Engine execution loop at key decision points.
|
||||||
///
|
///
|
||||||
/// All methods have default implementations that let the Engine
|
/// All methods have default implementations that let the Engine
|
||||||
/// proceed without intervention. Upper layers (e.g. Worker) provide
|
/// proceed without intervention. Callers provide richer implementations for
|
||||||
/// richer implementations for approval flows, permission checks, etc.
|
/// approval flows, permission checks, etc.
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait Interceptor: Send + Sync {
|
pub trait Interceptor: Send + Sync {
|
||||||
/// Called after receiving user input, before adding to history.
|
/// Called after receiving user input, before adding to history.
|
||||||
|
|
@ -149,7 +148,7 @@ pub trait Interceptor: Send + Sync {
|
||||||
///
|
///
|
||||||
/// Use this for inputs that arrive from outside the LLM and need
|
/// Use this for inputs that arrive from outside the LLM and need
|
||||||
/// to be reflected in the on-disk history — notifications,
|
/// to be reflected in the on-disk history — notifications,
|
||||||
/// cross-Worker events, system reminders. Do **not** use
|
/// external events, system reminders. Do **not** use
|
||||||
/// [`Self::pre_llm_request`] for that purpose: it mutates a
|
/// [`Self::pre_llm_request`] for that purpose: it mutates a
|
||||||
/// per-request clone, so any committed assistant response that
|
/// per-request clone, so any committed assistant response that
|
||||||
/// reacts to the injection would have no visible trigger on the
|
/// reacts to the injection would have no visible trigger on the
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ pub(crate) mod callback;
|
||||||
pub mod event;
|
pub mod event;
|
||||||
pub mod interceptor;
|
pub mod interceptor;
|
||||||
pub mod llm_client;
|
pub mod llm_client;
|
||||||
|
pub mod providers;
|
||||||
pub mod prune;
|
pub mod prune;
|
||||||
pub mod state;
|
pub mod state;
|
||||||
pub mod timeline;
|
pub mod timeline;
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,11 @@
|
||||||
//! `Scheme` 実装と通信層が要求する認証要件、および動的認証プロバイダ。
|
//! `Scheme` 実装と通信層が要求する認証要件、および動的認証プロバイダ。
|
||||||
//!
|
//!
|
||||||
//! マニフェスト側の型(`ModelConfig` / `SchemeKind` / `AuthRef`)は
|
|
||||||
//! `crates/manifest` に置き、llm-engine はそれを知らずに済む。
|
|
||||||
//! `AuthRequirement` は scheme が宣言する「この scheme はどんな認証を
|
//! `AuthRequirement` は scheme が宣言する「この scheme はどんな認証を
|
||||||
//! 期待するか」のランタイム記述で、manifest 側の `AuthRef` との
|
//! 期待するか」のランタイム記述で、設定ファイルや環境変数などから
|
||||||
//! 照合(`AuthRef → ResolvedAuth` 変換の適否)は `crates/provider`
|
//! [`super::transport::ResolvedAuth`] を組み立てる責務は呼び出し側にある。
|
||||||
//! で行う。
|
|
||||||
//!
|
//!
|
||||||
//! Codex OAuth のようにリクエスト毎にトークンが変わり得る認証は
|
//! リクエスト毎にトークンが変わり得る認証は [`AuthProvider`] trait を
|
||||||
//! [`AuthProvider`] trait を `crates/provider` 側で実装し、
|
//! 実装し、[`super::transport::ResolvedAuth::Custom`] 経由で transport に渡す。
|
||||||
//! [`super::transport::ResolvedAuth::Custom`] 経由で transport に渡す。
|
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use reqwest::header::{HeaderName, HeaderValue};
|
use reqwest::header::{HeaderName, HeaderValue};
|
||||||
|
|
@ -27,16 +23,15 @@ pub enum AuthRequirement {
|
||||||
XApiKey,
|
XApiKey,
|
||||||
/// クエリパラメータ `?<name>=<token>`(Gemini 形式)
|
/// クエリパラメータ `?<name>=<token>`(Gemini 形式)
|
||||||
QueryParam { name: &'static str },
|
QueryParam { name: &'static str },
|
||||||
/// 複合ヘッダ(Codex OAuth 等、`crates/provider` 側で解決)
|
/// 複合ヘッダ(呼び出し側が [`AuthProvider`] で解決)
|
||||||
Custom,
|
Custom,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// リクエスト毎に認証ヘッダを動的に組み立てるプロバイダ。
|
/// リクエスト毎に認証ヘッダを動的に組み立てるプロバイダ。
|
||||||
///
|
///
|
||||||
/// Codex OAuth のように access_token が refresh で更新されたり、
|
/// access token が refresh で更新されたり、複数ヘッダを同時に注入する
|
||||||
/// `ChatGPT-Account-Id` / `X-OpenAI-Fedramp` のような複数ヘッダを
|
/// 必要があるケースで使う。実体は呼び出し側に置き、llm-engine は
|
||||||
/// 同時に注入する必要があるケースで使う。実体は `crates/provider`
|
/// trait を知るだけ。
|
||||||
/// 側に置き、llm-engine は trait を知るだけ。
|
|
||||||
///
|
///
|
||||||
/// 返したヘッダはそのまま `HeaderMap` に挿入される。`Authorization`
|
/// 返したヘッダはそのまま `HeaderMap` に挿入される。`Authorization`
|
||||||
/// 含む scheme 既定の認証ヘッダは送出されないので、必要なら
|
/// 含む scheme 既定の認証ヘッダは送出されないので、必要なら
|
||||||
|
|
@ -45,13 +40,4 @@ pub enum AuthRequirement {
|
||||||
pub trait AuthProvider: Send + Sync + std::fmt::Debug {
|
pub trait AuthProvider: Send + Sync + std::fmt::Debug {
|
||||||
/// 1 リクエスト分の認証ヘッダを返す。refresh が必要なら内部で行う。
|
/// 1 リクエスト分の認証ヘッダを返す。refresh が必要なら内部で行う。
|
||||||
async fn headers(&self) -> Result<Vec<(HeaderName, HeaderValue)>, ClientError>;
|
async fn headers(&self) -> Result<Vec<(HeaderName, HeaderValue)>, ClientError>;
|
||||||
|
|
||||||
/// ChatGPT Codex backend 向けの複合認証かどうか。
|
|
||||||
///
|
|
||||||
/// transport は provider crate の具象型を知らないため、この hook だけで
|
|
||||||
/// Codex CLI 互換の wire behavior(conversation header / request compression 等)
|
|
||||||
/// を切り替える。
|
|
||||||
fn is_codex_backend(&self) -> bool {
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
//! LLM response stream を開く前の transient error 向けリトライポリシー。
|
//! LLM response stream を開く前の transient error 向けリトライポリシー。
|
||||||
//!
|
//!
|
||||||
//! Engine が `LlmClient::stream` の open error に対して `is_retryable` を見て
|
//! `LlmClient::stream` の open error に対して `is_retryable` を見て
|
||||||
//! retry / backoff / TUI event / cancellation をまとめて管理する。
|
//! retry / backoff / cancellation をまとめて管理する。
|
||||||
//! SSE 読み出し開始後の失敗は対象外。
|
//! SSE 読み出し開始後の失敗は対象外。
|
||||||
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
/// 指数バックオフ + ジッター + 累積タイムアウトを表すポリシー。
|
/// 指数バックオフ + ジッター + 累積タイムアウトを表すポリシー。
|
||||||
///
|
///
|
||||||
/// `Default` は llm-engine 全体の固定値を返す。manifest 経由の上書きが
|
/// `Default` は llm-engine 全体の固定値を返す。呼び出し側からの上書きが
|
||||||
/// 必要になったら拡張する(現状は不要 → `tickets/llm-engine-transient-retry.md`)。
|
/// 必要になったら拡張する。
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RetryPolicy {
|
pub struct RetryPolicy {
|
||||||
/// 指数の基準値。`base * 2^attempt` を `cap` で頭打ちにした上限から
|
/// 指数の基準値。`base * 2^attempt` を `cap` で頭打ちにした上限から
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Anthropic scheme の wire-level 既定 capability。
|
//! Anthropic scheme の wire-level 既定 capability。
|
||||||
//!
|
//!
|
||||||
//! モデル ID 固有のテーブル(`claude-*` など)は高レベル構築層
|
//! モデル ID 固有のテーブル(`claude-*` など)は client construction layer
|
||||||
//! (`provider::capability`)の責務。ここでは未知モデルでも「この wire で
|
//! の責務。ここでは未知モデルでも「この wire で
|
||||||
//! 安全に送れる最小共通項」を返すだけに留める。
|
//! 安全に送れる最小共通項」を返すだけに留める。
|
||||||
|
|
||||||
use crate::llm_client::capability::{
|
use crate::llm_client::capability::{
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Gemini scheme の wire-level 既定 capability。
|
//! Gemini scheme の wire-level 既定 capability。
|
||||||
//!
|
//!
|
||||||
//! モデル ID 固有のテーブル(`gemini-*` バージョン別の reasoning 有無)は
|
//! モデル ID 固有のテーブル(`gemini-*` バージョン別の reasoning 有無)は
|
||||||
//! 高レベル構築層(`provider::capability`)の責務。ここでは wire の
|
//! client construction layer の責務。ここでは wire の
|
||||||
//! 保守的 default のみ。
|
//! 保守的 default のみ。
|
||||||
|
|
||||||
use crate::llm_client::capability::{
|
use crate::llm_client::capability::{
|
||||||
|
|
|
||||||
|
|
@ -45,8 +45,8 @@ pub trait Scheme: Clone + Send + Sync + 'static {
|
||||||
/// プロバイダもあるため、モデル ID を受け取る。
|
/// プロバイダもあるため、モデル ID を受け取る。
|
||||||
fn path(&self, model_id: &str) -> String;
|
fn path(&self, model_id: &str) -> String;
|
||||||
|
|
||||||
/// この scheme が要求する認証形式。`build_client` 時に
|
/// この scheme が要求する認証形式。呼び出し側は client 構築時に
|
||||||
/// `manifest::AuthRef` と照合する。
|
/// 設定された認証情報と照合する。
|
||||||
fn required_auth(&self) -> AuthRequirement;
|
fn required_auth(&self) -> AuthRequirement;
|
||||||
|
|
||||||
/// `Content-Type` 以外の追加ヘッダ。`anthropic-version` / `anthropic-beta` 等。
|
/// `Content-Type` 以外の追加ヘッダ。`anthropic-version` / `anthropic-beta` 等。
|
||||||
|
|
@ -78,8 +78,7 @@ pub trait Scheme: Clone + Send + Sync + 'static {
|
||||||
|
|
||||||
/// scheme 既定の capability。モデル ID に関係なく、この wire で
|
/// scheme 既定の capability。モデル ID に関係なく、この wire で
|
||||||
/// 安全に送れる最小共通項を返す。既知モデル ID の能力テーブルは
|
/// 安全に送れる最小共通項を返す。既知モデル ID の能力テーブルは
|
||||||
/// `provider::capability::lookup` 側(高レベル構築層)の責務で、
|
/// 高レベルの client 構築層の責務で、scheme はここには関与しない。
|
||||||
/// scheme はここには関与しない。
|
|
||||||
fn default_capability(&self) -> ModelCapability;
|
fn default_capability(&self) -> ModelCapability;
|
||||||
|
|
||||||
/// scheme 側でサポートしていない `RequestConfig` フィールドを
|
/// scheme 側でサポートしていない `RequestConfig` フィールドを
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! OpenAI Chat Completions scheme の wire-level 既定 capability。
|
//! OpenAI Chat Completions scheme の wire-level 既定 capability。
|
||||||
//!
|
//!
|
||||||
//! モデル ID 固有のテーブル(`gpt-5` 系など)は高レベル構築層
|
//! モデル ID 固有のテーブル(`gpt-5` 系など)は client construction layer
|
||||||
//! (`provider::capability`)の責務。ここでは wire の保守的 default のみ。
|
//! の責務。ここでは wire の保守的 default のみ。
|
||||||
|
|
||||||
use crate::llm_client::capability::{
|
use crate::llm_client::capability::{
|
||||||
CacheStrategy, ModelCapability, StructuredOutput, ToolCallingSupport,
|
CacheStrategy, ModelCapability, StructuredOutput, ToolCallingSupport,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! OpenAI Responses scheme の wire-level 既定 capability。
|
//! OpenAI Responses scheme の wire-level 既定 capability。
|
||||||
//!
|
//!
|
||||||
//! モデル ID 固有のテーブル(`gpt-5` / `codex-` 系など)は高レベル構築層
|
//! モデル ID 固有の能力テーブルは高レベルの client 構築層の責務。
|
||||||
//! (`provider::capability`)の責務。ここでは wire の保守的 default のみ。
|
//! ここでは wire の保守的 default のみ。
|
||||||
|
|
||||||
use crate::llm_client::capability::{
|
use crate::llm_client::capability::{
|
||||||
CacheStrategy, ModelCapability, StructuredOutput, ToolCallingSupport,
|
CacheStrategy, ModelCapability, StructuredOutput, ToolCallingSupport,
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! `response.*` 名前空間の SSE を共通の [`Event`](crate::llm_client::event::Event)
|
//! `response.*` 名前空間の SSE を共通の [`Event`](crate::llm_client::event::Event)
|
||||||
//! に変換する。Responses の (output_index, content_index) 2 次元座標と
|
//! に変換する。Responses の (output_index, content_index) 2 次元座標と
|
||||||
//! yoi 側 1 次元 `BlockStart/Delta/Stop::index` のマッピングは
|
//! この crate の 1 次元 `BlockStart/Delta/Stop::index` のマッピングは
|
||||||
//! [`OpenAIResponsesState`] が保持する。
|
//! [`OpenAIResponsesState`] が保持する。
|
||||||
|
|
||||||
use std::collections::{BTreeMap, HashMap};
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
//!
|
//!
|
||||||
//! Chat Completions とは別物の item-based wire format。reasoning item と
|
//! Chat Completions とは別物の item-based wire format。reasoning item と
|
||||||
//! function_call item が first-class で、SSE イベントも `response.*` 名前空間で
|
//! function_call item が first-class で、SSE イベントも `response.*` 名前空間で
|
||||||
//! 流れる。ChatGPT OAuth 経路 (codex) は本 scheme 必須。
|
//! 流れる。
|
||||||
//!
|
//!
|
||||||
//! - リクエスト JSON 生成: [`request`]
|
//! - リクエスト JSON 生成: [`request`]
|
||||||
//! - SSE イベントパース → [`Event`](crate::llm_client::event::Event) 変換: [`events`]
|
//! - SSE イベントパース → [`Event`](crate::llm_client::event::Event) 変換: [`events`]
|
||||||
|
|
@ -19,10 +19,10 @@ pub use scheme_impl::OpenAIResponsesState;
|
||||||
/// `store` / `include_encrypted_content` / `send_max_output_tokens` /
|
/// `store` / `include_encrypted_content` / `send_max_output_tokens` /
|
||||||
/// `send_sampling_params` は scheme 固定の wire 設定で、デフォルトは
|
/// `send_sampling_params` は scheme 固定の wire 設定で、デフォルトは
|
||||||
/// 公式 OpenAI Responses API 向け (stateless + ZDR + `max_output_tokens`
|
/// 公式 OpenAI Responses API 向け (stateless + ZDR + `max_output_tokens`
|
||||||
/// / `temperature` / `top_p` 送出可)。ChatGPT backend (codex-oauth) の
|
/// / `temperature` / `top_p` 送出可)。受理パラメータが subset の
|
||||||
/// ように受理パラメータが subset の経路では provider 層で
|
/// 互換 backend では client 構築層で `send_max_output_tokens=false` /
|
||||||
/// `send_max_output_tokens=false` / `send_sampling_params=false` に
|
/// `send_sampling_params=false` に上書きする。`ModelCapability` には
|
||||||
/// 上書きする。`ModelCapability` には入れない(モデル能力ではなく wire policy)。
|
/// 入れない(モデル能力ではなく wire policy)。
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct OpenAIResponsesScheme {
|
pub struct OpenAIResponsesScheme {
|
||||||
/// サーバ側に response を保存するか。ZDR/stateless 運用では `false`。
|
/// サーバ側に response を保存するか。ZDR/stateless 運用では `false`。
|
||||||
|
|
@ -31,12 +31,12 @@ pub struct OpenAIResponsesScheme {
|
||||||
/// `store=false` で reasoning を使うなら必須。
|
/// `store=false` で reasoning を使うなら必須。
|
||||||
pub include_encrypted_content: bool,
|
pub include_encrypted_content: bool,
|
||||||
/// `max_output_tokens` を body に載せるか。公式 OpenAI Responses API は
|
/// `max_output_tokens` を body に載せるか。公式 OpenAI Responses API は
|
||||||
/// 受理するが、ChatGPT backend (codex-oauth) は `Unsupported parameter`
|
/// 受理するが、互換 backend によっては `Unsupported parameter` で
|
||||||
/// で 400 を返すため、その経路では `false` にする。
|
/// 400 を返すため、その経路では `false` にする。
|
||||||
pub send_max_output_tokens: bool,
|
pub send_max_output_tokens: bool,
|
||||||
/// `temperature` / `top_p` を body に載せるか。公式 OpenAI Responses API
|
/// `temperature` / `top_p` を body に載せるか。公式 OpenAI Responses API
|
||||||
/// は受理するが、ChatGPT backend (codex-oauth) は `Unsupported parameter`
|
/// は受理するが、互換 backend によっては `Unsupported parameter` で
|
||||||
/// で 400 を返すため、その経路では `false` にする。
|
/// 400 を返すため、その経路では `false` にする。
|
||||||
pub send_sampling_params: bool,
|
pub send_sampling_params: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,21 +38,21 @@ pub(crate) struct ResponsesRequest {
|
||||||
/// `["reasoning.encrypted_content"]` 等。
|
/// `["reasoning.encrypted_content"]` 等。
|
||||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||||
pub include: Vec<&'static str>,
|
pub include: Vec<&'static str>,
|
||||||
/// 公式 OpenAI Responses API では受理されるが、ChatGPT backend
|
/// 公式 OpenAI Responses API では受理されるが、互換 backend によっては
|
||||||
/// (codex-oauth) は 400 で弾く。scheme の `send_max_output_tokens`
|
/// 400 で弾く。scheme の `send_max_output_tokens` が `false` のときは
|
||||||
/// が `false` のときは `None` のまま送る (skip_serializing_if で除外)。
|
/// `None` のまま送る (skip_serializing_if で除外)。
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub max_output_tokens: Option<u32>,
|
pub max_output_tokens: Option<u32>,
|
||||||
/// 公式 OpenAI Responses API では受理されるが、ChatGPT backend
|
/// 公式 OpenAI Responses API では受理されるが、互換 backend によっては
|
||||||
/// (codex-oauth) は `temperature` / `top_p` を 400 で弾く。scheme の
|
/// `temperature` / `top_p` を 400 で弾く。scheme の
|
||||||
/// `send_sampling_params` が `false` のときは `None` のまま送る。
|
/// `send_sampling_params` が `false` のときは `None` のまま送る。
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub temperature: Option<f32>,
|
pub temperature: Option<f32>,
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub top_p: Option<f32>,
|
pub top_p: Option<f32>,
|
||||||
/// 会話単位の安定キー。ChatGPT backend (codex-oauth) は明示キーが
|
/// 会話単位の安定キー。明示キーを必要とする backend では、
|
||||||
/// 無いとプロンプトキャッシュがほぼ効かない。worker 側は `SegmentId`
|
/// 呼び出し側が安定した conversation identifier を渡す。
|
||||||
/// を渡す。`Request::cache_key` が `None` のときはキー自体を送らない。
|
/// `Request::cache_key` が `None` のときはキー自体を送らない。
|
||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub prompt_cache_key: Option<String>,
|
pub prompt_cache_key: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -74,10 +74,9 @@ pub(crate) struct ReasoningConfig {
|
||||||
#[serde(tag = "type", rename_all = "snake_case")]
|
#[serde(tag = "type", rename_all = "snake_case")]
|
||||||
pub(crate) enum InputItem {
|
pub(crate) enum InputItem {
|
||||||
/// 会話メッセージ。user / assistant / developer のいずれか。
|
/// 会話メッセージ。user / assistant / developer のいずれか。
|
||||||
/// `Role::System` items は `developer` として投影する(ChatGPT
|
/// `Role::System` items は `developer` として投影する。OpenAI
|
||||||
/// backend が `role: "system"` を拒否するため。Codex CLI も
|
/// Responses 互換 backend の一部は `role: "system"` を拒否するため、
|
||||||
/// system 相当の挿入には DeveloperInstructions = `role: "developer"`
|
/// system 相当の挿入には `role: "developer"` を使う。
|
||||||
/// を使う)。
|
|
||||||
Message {
|
Message {
|
||||||
role: &'static str,
|
role: &'static str,
|
||||||
content: Vec<InputContent>,
|
content: Vec<InputContent>,
|
||||||
|
|
@ -403,10 +402,9 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn system_role_item_is_projected_as_developer() {
|
fn system_role_item_is_projected_as_developer() {
|
||||||
// ChatGPT backend (codex-oauth) は input[] の `role: "system"` を
|
// Some compatible backends reject `role: "system"` in input[].
|
||||||
// "System messages are not allowed" で 400 拒否する。in-conversation
|
// Project in-conversation system notes as `role: "developer"` so
|
||||||
// な system note (notify / fs_view auto-read / compaction summary) は
|
// both official and compatible backends can accept them.
|
||||||
// `role: "developer"` として投影し、両 backend で受理されるようにする。
|
|
||||||
let scheme = OpenAIResponsesScheme::new();
|
let scheme = OpenAIResponsesScheme::new();
|
||||||
let req = Request::new()
|
let req = Request::new()
|
||||||
.user("hi")
|
.user("hi")
|
||||||
|
|
@ -523,11 +521,9 @@ mod tests {
|
||||||
fn reasoning_summary_field_is_always_serialized() {
|
fn reasoning_summary_field_is_always_serialized() {
|
||||||
// Responses API は reasoning item に `summary` を必須で要求する。
|
// Responses API は reasoning item に `summary` を必須で要求する。
|
||||||
// summary が空でも wire 上に `summary: []` として残らないと、
|
// summary が空でも wire 上に `summary: []` として残らないと、
|
||||||
// ChatGPT backend (codex-oauth) が
|
// backend によっては missing required parameter として拒否される。
|
||||||
// 400 invalid_request_error: Missing required parameter:
|
// reasoning effort 未指定のターンでは summary text が付かないことが
|
||||||
// 'input[N].summary'.
|
// あるため、空のままでも skip しないこと。
|
||||||
// で弾く。GPT-5 + reasoning effort 未指定のターンでは summary text
|
|
||||||
// が付かないことがあるため、空のままでも skip しないこと。
|
|
||||||
let scheme = OpenAIResponsesScheme::new();
|
let scheme = OpenAIResponsesScheme::new();
|
||||||
let item = Item::reasoning("").with_encrypted_content("ENC");
|
let item = Item::reasoning("").with_encrypted_content("ENC");
|
||||||
let req = Request::new().user("hi").item(item);
|
let req = Request::new().user("hi").item(item);
|
||||||
|
|
|
||||||
|
|
@ -20,9 +20,8 @@ impl Scheme for OpenAIResponsesScheme {
|
||||||
type State = OpenAIResponsesState;
|
type State = OpenAIResponsesState;
|
||||||
|
|
||||||
fn default_base_url(&self) -> &'static str {
|
fn default_base_url(&self) -> &'static str {
|
||||||
// `/v1` は base_url 側に寄せる。ChatGPT OAuth 経由のときは
|
// `/v1` は base_url 側に寄せる。互換 backend を使う場合も、
|
||||||
// `https://chatgpt.com/backend-api/codex` を base にすれば同じ
|
// base URL を差し替えるだけで同じ `/responses` path を使える。
|
||||||
// `/responses` path で両系統を吸収できる(Codex CLI 準拠)。
|
|
||||||
"https://api.openai.com/v1"
|
"https://api.openai.com/v1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,27 +58,29 @@ impl Scheme for OpenAIResponsesScheme {
|
||||||
|
|
||||||
fn validate_config(&self, config: &RequestConfig) -> Vec<ConfigWarning> {
|
fn validate_config(&self, config: &RequestConfig) -> Vec<ConfigWarning> {
|
||||||
let mut warnings = Vec::new();
|
let mut warnings = Vec::new();
|
||||||
// ChatGPT backend (codex-oauth) は `max_output_tokens` を 400 で弾く。
|
// Some compatible backends reject `max_output_tokens` with HTTP 400.
|
||||||
// scheme 構築時に `send_max_output_tokens=false` で組まれていれば
|
// If the scheme was built with `send_max_output_tokens=false`, body
|
||||||
// body 投影は止まっているので、ユーザの意図が落ちることだけを通知する。
|
// projection is already disabled; only notify that the user's intent
|
||||||
|
// was dropped.
|
||||||
if !self.send_max_output_tokens && config.max_tokens.is_some() {
|
if !self.send_max_output_tokens && config.max_tokens.is_some() {
|
||||||
warnings.push(ConfigWarning::unsupported(
|
warnings.push(ConfigWarning::unsupported(
|
||||||
"max_tokens",
|
"max_tokens",
|
||||||
"OpenAI Responses (ChatGPT backend)",
|
"OpenAI Responses 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 !self.send_sampling_params {
|
||||||
if config.temperature.is_some() {
|
if config.temperature.is_some() {
|
||||||
warnings.push(ConfigWarning::unsupported(
|
warnings.push(ConfigWarning::unsupported(
|
||||||
"temperature",
|
"temperature",
|
||||||
"OpenAI Responses (ChatGPT backend)",
|
"OpenAI Responses compatible backend",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if config.top_p.is_some() {
|
if config.top_p.is_some() {
|
||||||
warnings.push(ConfigWarning::unsupported(
|
warnings.push(ConfigWarning::unsupported(
|
||||||
"top_p",
|
"top_p",
|
||||||
"OpenAI Responses (ChatGPT backend)",
|
"OpenAI Responses compatible backend",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
//! `HttpTransport<S: Scheme>`: すべての LLM wire scheme を共通の 1 本の
|
//! `HttpTransport<S: Scheme>`: すべての LLM wire scheme を共通の 1 本の
|
||||||
//! HTTP クライアントで扱う。
|
//! HTTP クライアントで扱う。
|
||||||
//!
|
//!
|
||||||
//! 旧 `providers/{anthropic,openai,gemini,ollama}.rs` を置き換える。
|
//! scheme 固有の差分は [`Scheme`] trait 実装に委譲し、backend 固有の
|
||||||
//! scheme 固有の差分は [`Scheme`] trait 実装に委譲する。
|
//! HTTP policy は [`TransportPolicy`] で明示的に差し込む。
|
||||||
|
|
||||||
use std::pin::Pin;
|
use std::pin::Pin;
|
||||||
use std::sync::Arc;
|
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_STREAM_OPEN_TIMEOUT: Duration = Duration::from_secs(20);
|
||||||
pub const DEFAULT_FIRST_STREAM_EVENT_TIMEOUT: Duration = Duration::from_secs(30);
|
pub const DEFAULT_FIRST_STREAM_EVENT_TIMEOUT: Duration = Duration::from_secs(30);
|
||||||
|
|
||||||
/// `AuthRef` を解決したランタイム表現。`crates/provider` が構築する。
|
/// 認証設定をリクエスト時に使える形へ解決したランタイム表現。
|
||||||
///
|
///
|
||||||
/// - `None`: 認証ヘッダを送らない(Ollama 等の opt-out)
|
/// - `None`: 認証ヘッダを送らない(Ollama 等の opt-out)
|
||||||
/// - `ApiKey`: 静的な API key 文字列
|
/// - `ApiKey`: 静的な API key 文字列
|
||||||
/// - `Custom`: リクエスト毎に動的にヘッダを組み立てる(Codex OAuth 等)
|
/// - `Custom`: リクエスト毎に動的にヘッダを組み立てる
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum ResolvedAuth {
|
pub enum ResolvedAuth {
|
||||||
None,
|
None,
|
||||||
|
|
@ -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<String> {
|
fn header_value_for_diagnostics(headers: &HeaderMap, name: &str) -> Option<String> {
|
||||||
headers
|
headers
|
||||||
.get(name)
|
.get(name)
|
||||||
|
|
@ -120,6 +185,7 @@ pub struct HttpTransport<S: Scheme> {
|
||||||
base_url: String,
|
base_url: String,
|
||||||
auth: ResolvedAuth,
|
auth: ResolvedAuth,
|
||||||
capability: ModelCapability,
|
capability: ModelCapability,
|
||||||
|
policy: TransportPolicy,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: Scheme> HttpTransport<S> {
|
impl<S: Scheme> HttpTransport<S> {
|
||||||
|
|
@ -141,9 +207,16 @@ impl<S: Scheme> HttpTransport<S> {
|
||||||
base_url,
|
base_url,
|
||||||
auth,
|
auth,
|
||||||
capability,
|
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 クライアントを差し込む(テスト等)。
|
/// カスタム HTTP クライアントを差し込む(テスト等)。
|
||||||
pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
|
pub fn with_http_client(mut self, client: reqwest::Client) -> Self {
|
||||||
self.http_client = client;
|
self.http_client = client;
|
||||||
|
|
@ -206,13 +279,6 @@ impl<S: Scheme> HttpTransport<S> {
|
||||||
Ok(headers)
|
Ok(headers)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_codex_backend(&self) -> bool {
|
|
||||||
match &self.auth {
|
|
||||||
ResolvedAuth::Custom(provider) => provider.is_codex_backend(),
|
|
||||||
_ => false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_stream_headers(
|
fn apply_stream_headers(
|
||||||
&self,
|
&self,
|
||||||
headers: &mut HeaderMap,
|
headers: &mut HeaderMap,
|
||||||
|
|
@ -220,19 +286,26 @@ impl<S: Scheme> HttpTransport<S> {
|
||||||
) -> Result<(), ClientError> {
|
) -> Result<(), ClientError> {
|
||||||
headers.insert(ACCEPT, HeaderValue::from_static("text/event-stream"));
|
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 Some(cache_key) = request.cache_key.as_deref()
|
||||||
{
|
{
|
||||||
let value = HeaderValue::from_str(cache_key).map_err(|e| {
|
let value = HeaderValue::from_str(cache_key).map_err(|e| {
|
||||||
ClientError::Config(format!("invalid Codex conversation header: {e}"))
|
ClientError::Config(format!("invalid 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("session-id"), value.clone());
|
||||||
headers.insert(HeaderName::from_static("thread-id"), value.clone());
|
if include_thread_id {
|
||||||
headers.insert(HeaderName::from_static("session_id"), value.clone());
|
headers.insert(HeaderName::from_static("thread-id"), value.clone());
|
||||||
headers.insert(HeaderName::from_static("x-client-request-id"), value);
|
}
|
||||||
|
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(())
|
Ok(())
|
||||||
|
|
@ -243,19 +316,22 @@ impl<S: Scheme> HttpTransport<S> {
|
||||||
body: &serde_json::Value,
|
body: &serde_json::Value,
|
||||||
headers: &mut HeaderMap,
|
headers: &mut HeaderMap,
|
||||||
) -> Result<RequestBody, ClientError> {
|
) -> Result<RequestBody, ClientError> {
|
||||||
if !self.is_codex_backend() {
|
match self.policy.request_body_encoding {
|
||||||
return Ok(RequestBody::Json(body.clone()));
|
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<S: Scheme + Clone> Clone for HttpTransport<S> {
|
||||||
base_url: self.base_url.clone(),
|
base_url: self.base_url.clone(),
|
||||||
auth: self.auth.clone(),
|
auth: self.auth.clone(),
|
||||||
capability: self.capability.clone(),
|
capability: self.capability.clone(),
|
||||||
|
policy: self.policy.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -447,7 +524,8 @@ impl<S: Scheme + Clone + 'static> LlmClient for HttpTransport<S> {
|
||||||
"path": path,
|
"path": path,
|
||||||
"auth_kind": auth_kind(&self.auth),
|
"auth_kind": auth_kind(&self.auth),
|
||||||
"required_auth": format!("{:?}", self.scheme.required_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(),
|
"cache_key_present": request.cache_key.is_some(),
|
||||||
"stream_open_timeout_ms": DEFAULT_STREAM_OPEN_TIMEOUT.as_millis() as u64,
|
"stream_open_timeout_ms": DEFAULT_STREAM_OPEN_TIMEOUT.as_millis() as u64,
|
||||||
}),
|
}),
|
||||||
|
|
@ -678,9 +756,7 @@ mod tests {
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct TestAuthProvider {
|
struct TestAuthProvider;
|
||||||
codex: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl AuthProvider for TestAuthProvider {
|
impl AuthProvider for TestAuthProvider {
|
||||||
|
|
@ -696,10 +772,6 @@ mod tests {
|
||||||
),
|
),
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_codex_backend(&self) -> bool {
|
|
||||||
self.codex
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -828,10 +900,9 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn codex_backend_adds_conversation_headers_and_zstd_body() {
|
async fn transport_policy_adds_conversation_headers_and_zstd_body() {
|
||||||
let transport = transport(ResolvedAuth::Custom(Arc::new(TestAuthProvider {
|
let transport = transport(ResolvedAuth::Custom(Arc::new(TestAuthProvider)))
|
||||||
codex: true,
|
.with_transport_policy(TransportPolicy::openai_compatible_zstd());
|
||||||
})));
|
|
||||||
let request = Request::new().user("hello").cache_key("segment-123");
|
let request = Request::new().user("hello").cache_key("segment-123");
|
||||||
let mut headers = transport.build_headers().await.unwrap();
|
let mut headers = transport.build_headers().await.unwrap();
|
||||||
transport
|
transport
|
||||||
|
|
@ -870,7 +941,7 @@ mod tests {
|
||||||
raw_json_bytes,
|
raw_json_bytes,
|
||||||
} = encoded
|
} = encoded
|
||||||
else {
|
else {
|
||||||
panic!("Codex backend request body must be zstd-compressed");
|
panic!("transport policy should zstd-compress request body");
|
||||||
};
|
};
|
||||||
assert!(raw_json_bytes > 0);
|
assert!(raw_json_bytes > 0);
|
||||||
let decoded = zstd::stream::decode_all(std::io::Cursor::new(compressed)).unwrap();
|
let decoded = zstd::stream::decode_all(std::io::Cursor::new(compressed)).unwrap();
|
||||||
|
|
@ -879,7 +950,7 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[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 transport = transport(ResolvedAuth::ApiKey("api-key".to_string()));
|
||||||
let request = Request::new().user("hello").cache_key("segment-123");
|
let request = Request::new().user("hello").cache_key("segment-123");
|
||||||
let mut headers = transport.build_headers().await.unwrap();
|
let mut headers = transport.build_headers().await.unwrap();
|
||||||
|
|
@ -901,7 +972,7 @@ mod tests {
|
||||||
assert!(headers.get(CONTENT_ENCODING).is_none());
|
assert!(headers.get(CONTENT_ENCODING).is_none());
|
||||||
|
|
||||||
let RequestBody::Json(decoded) = encoded else {
|
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");
|
assert_eq!(decoded["prompt_cache_key"], "segment-123");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! LLM Client Common Types
|
//! LLM Client Common Types
|
||||||
//!
|
//!
|
||||||
//! Core conversation types for yoi's LLM interaction model.
|
//! Core conversation types for LLM interaction.
|
||||||
//! The core abstraction is `Item` which represents different types of conversation elements:
|
//! The core abstraction is `Item` which represents different types of conversation elements:
|
||||||
//! - Message items (user/assistant messages with content parts)
|
//! - Message items (user/assistant messages with content parts)
|
||||||
//! - ToolCall items (tool invocations)
|
//! - ToolCall items (tool invocations)
|
||||||
|
|
@ -521,9 +521,8 @@ pub struct Request {
|
||||||
/// Providers without prompt caching ignore the field.
|
/// Providers without prompt caching ignore the field.
|
||||||
pub cache_anchor: Option<usize>,
|
pub cache_anchor: Option<usize>,
|
||||||
/// 会話単位の安定キー。`prompt_cache_key` として送られる
|
/// 会話単位の安定キー。`prompt_cache_key` として送られる
|
||||||
/// (OpenAI Responses)。ChatGPT backend (codex-oauth) は明示キーが
|
/// (OpenAI Responses)。明示キーを必要とする backend では、
|
||||||
/// 無いと org/project ハッシュ衝突でプロンプトキャッシュが
|
/// 呼び出し側が安定した conversation identifier を渡す。
|
||||||
/// ほぼヒットしないため、worker 側で `SegmentId` を渡す運用を想定。
|
|
||||||
/// `cache_anchor` と違い名前空間キーであり、`prefix anchor` とは
|
/// `cache_anchor` と違い名前空間キーであり、`prefix anchor` とは
|
||||||
/// 別の概念。`cache_anchor` を読まない provider と同じく、
|
/// 別の概念。`cache_anchor` を読まない provider と同じく、
|
||||||
/// `prompt_cache_key` を持たない provider は無視する。
|
/// `prompt_cache_key` を持たない provider は無視する。
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! `~/.codex/auth.json` の読み書き。
|
//! `~/.codex/auth.json` の読み書き。
|
||||||
//!
|
//!
|
||||||
//! Codex CLI と schema を共有するが、yoi は知らないフィールドを
|
//! Codex CLI と schema を共有するが、知らないフィールドを失わないよう
|
||||||
//! 失わないようファイル全体を `serde_json::Value` で保持し、必要箇所
|
//! ファイル全体を `serde_json::Value` で保持し、必要箇所
|
||||||
//! のみアクセスする。書込は `mode 0o600` を再設定(Codex CLI 同様)、
|
//! のみアクセスする。書込は `mode 0o600` を再設定(Codex CLI 同様)、
|
||||||
//! ファイルロックは取らない(manager 側で guarded reload)。
|
//! ファイルロックは取らない(manager 側で guarded reload)。
|
||||||
|
|
||||||
|
|
@ -106,7 +106,7 @@ pub async fn load(path: &Path) -> Result<AuthSnapshot, CodexAuthError> {
|
||||||
/// 既存ファイルを再読込し、`tokens.{id_token,access_token,refresh_token}` と
|
/// 既存ファイルを再読込し、`tokens.{id_token,access_token,refresh_token}` と
|
||||||
/// `last_refresh` を更新して書き戻す。Codex CLI の `persist_tokens` 相当。
|
/// `last_refresh` を更新して書き戻す。Codex CLI の `persist_tokens` 相当。
|
||||||
///
|
///
|
||||||
/// 並行する Codex CLI / 別 yoi プロセスが先に refresh していた場合の
|
/// 並行する Codex CLI / 別プロセスが先に refresh していた場合の
|
||||||
/// fields を保護するため、書込前に再 load して merge する。
|
/// fields を保護するため、書込前に再 load して merge する。
|
||||||
pub async fn persist_refreshed(
|
pub async fn persist_refreshed(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
|
|
@ -6,7 +6,7 @@
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use llm_engine::llm_client::ClientError;
|
use crate::llm_client::ClientError;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Debug, Error)]
|
#[derive(Debug, Error)]
|
||||||
|
|
@ -4,11 +4,11 @@
|
||||||
//!
|
//!
|
||||||
//! 設計:
|
//! 設計:
|
||||||
//!
|
//!
|
||||||
//! - llm-engine は [`AuthProvider`] trait しか知らず、実体である
|
//! - HTTP transport は [`AuthProvider`] trait だけを見て、実体である
|
||||||
//! [`CodexAuthProvider`] はこのクレートに置く(feedback_llm_engine_scope)
|
//! [`CodexAuthProvider`] はこの optional module に置く
|
||||||
//! - access_token JWT の `exp` を読み、`now` 以下で proactive refresh
|
//! - access_token JWT の `exp` を読み、`now` 以下で proactive refresh
|
||||||
//! (Codex CLI と同じバッファなし)
|
//! (Codex CLI と同じバッファなし)
|
||||||
//! - 並行する Codex CLI / 別 yoi の refresh と取り違えないよう、
|
//! - 並行する Codex CLI / 別プロセスの refresh と取り違えないよう、
|
||||||
//! refresh 直前に再 load して account_id 一致を確認(guarded reload)
|
//! refresh 直前に再 load して account_id 一致を確認(guarded reload)
|
||||||
//! - ファイルロックは取らず、書込前に再 load + diff merge で吸収
|
//! - ファイルロックは取らず、書込前に再 load + diff merge で吸収
|
||||||
//! - Codex の Keyring storage は対象外。auth.json 不在ならエラーで案内
|
//! - Codex の Keyring storage は対象外。auth.json 不在ならエラーで案内
|
||||||
|
|
@ -21,9 +21,9 @@ mod refresh;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::llm_client::{ClientError, auth::AuthProvider};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use chrono::{Duration, Utc};
|
use chrono::{Duration, Utc};
|
||||||
use llm_engine::llm_client::{ClientError, auth::AuthProvider};
|
|
||||||
use reqwest::header::{HeaderName, HeaderValue};
|
use reqwest::header::{HeaderName, HeaderValue};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
|
|
@ -152,7 +152,7 @@ impl CodexAuthProvider {
|
||||||
.map_err(|e| CodexAuthError::InvalidHeader(format!("ChatGPT-Account-Id: {e}")))?;
|
.map_err(|e| CodexAuthError::InvalidHeader(format!("ChatGPT-Account-Id: {e}")))?;
|
||||||
out.push((HeaderName::from_static("chatgpt-account-id"), acc_val));
|
out.push((HeaderName::from_static("chatgpt-account-id"), acc_val));
|
||||||
|
|
||||||
// Cloudflare WAF は ChatGPT backend アクセス元を `originator` /
|
// Cloudflare WAF は互換 backend アクセス元を `originator` /
|
||||||
// `User-Agent` で識別する。Codex CLI が送る固定値を流用しないと
|
// `User-Agent` で識別する。Codex CLI が送る固定値を流用しないと
|
||||||
// HTML challenge (403) を返されて SSE に到達できない。
|
// HTML challenge (403) を返されて SSE に到達できない。
|
||||||
out.push((
|
out.push((
|
||||||
|
|
@ -188,10 +188,6 @@ impl AuthProvider for CodexAuthProvider {
|
||||||
.map_err(CodexAuthError::to_client_error)?;
|
.map_err(CodexAuthError::to_client_error)?;
|
||||||
Self::build_headers(&snap).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。
|
/// `access_token` の JWT `exp` を見て、期限切れなら true。
|
||||||
|
|
@ -80,7 +80,7 @@ async fn response_with_timeout(
|
||||||
.await
|
.await
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
CodexAuthError::RefreshTransient(format!(
|
CodexAuthError::RefreshTransient(format!(
|
||||||
"codex_oauth_refresh timed out after {}s",
|
"oauth_token_refresh timed out after {}s",
|
||||||
timeout.as_secs()
|
timeout.as_secs()
|
||||||
))
|
))
|
||||||
})?
|
})?
|
||||||
4
crates/llm-engine/src/providers/mod.rs
Normal file
4
crates/llm-engine/src/providers/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
//! Optional built-in provider/backend helpers.
|
||||||
|
|
||||||
|
#[cfg(feature = "codex")]
|
||||||
|
pub mod codex;
|
||||||
|
|
@ -8,8 +8,8 @@
|
||||||
//!
|
//!
|
||||||
//! Prune は **コンテキスト射影** であり、history の変換ではない。
|
//! Prune は **コンテキスト射影** であり、history の変換ではない。
|
||||||
//! この crate が提供するのは pure な候補抽出 [`prunable_indices`] のみで、
|
//! この crate が提供するのは pure な候補抽出 [`prunable_indices`] のみで、
|
||||||
//! 射影の適用は上位層(`worker::prune_hook` 等)が LLM に送る一時コンテキスト
|
//! 射影の適用は LLM に送る一時コンテキストに対してだけ行う。
|
||||||
//! に対してだけ行う。Engine の永続履歴は決して変更されない。
|
//! Engine の永続履歴は決して変更されない。
|
||||||
//!
|
//!
|
||||||
//! 保護境界は末尾 token budget で決めるが、この crate は usage 履歴を
|
//! 保護境界は末尾 token budget で決めるが、この crate は usage 履歴を
|
||||||
//! 所有しない。prefix ごとの token 推定値と savings 推定は上位層から
|
//! 所有しない。prefix ごとの token 推定値と savings 推定は上位層から
|
||||||
|
|
@ -75,7 +75,7 @@ pub enum PruneDecision {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Optional observer invoked after each prune evaluation, regardless of
|
/// Optional observer invoked after each prune evaluation, regardless of
|
||||||
/// branch. Worker 等の上位層が install して metrics を発行する。
|
/// branch. Callers can install this to publish metrics or traces.
|
||||||
pub type PruneObserver = Box<dyn Fn(&PruneEvaluation) + Send + Sync>;
|
pub type PruneObserver = Box<dyn Fn(&PruneEvaluation) + Send + Sync>;
|
||||||
|
|
||||||
/// Configuration for the Prune algorithm.
|
/// Configuration for the Prune algorithm.
|
||||||
|
|
|
||||||
|
|
@ -130,13 +130,13 @@ mod tests {
|
||||||
let mut timeline = Timeline::new();
|
let mut timeline = Timeline::new();
|
||||||
timeline.on_tool_use_block(collector.clone());
|
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));
|
timeline.dispatch(&Event::tool_use_stop(0));
|
||||||
|
|
||||||
let calls = collector.take_collected();
|
let calls = collector.take_collected();
|
||||||
assert_eq!(calls.len(), 1);
|
assert_eq!(calls.len(), 1);
|
||||||
assert_eq!(calls[0].id, "tool_empty");
|
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!(calls[0].input.is_object());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
calls[0].input,
|
calls[0].input,
|
||||||
|
|
|
||||||
|
|
@ -75,10 +75,9 @@ impl ToolServerHandle {
|
||||||
/// Execute all pending factories and register the resulting tools.
|
/// Execute all pending factories and register the resulting tools.
|
||||||
///
|
///
|
||||||
/// Called implicitly by `Engine::lock()` before the first turn.
|
/// Called implicitly by `Engine::lock()` before the first turn.
|
||||||
/// Exposed as `pub` so higher layers (e.g. Worker) can force-materialise
|
/// Exposed as `pub` so callers can force-materialise tools earlier —
|
||||||
/// tools earlier — for example when building a system-prompt template
|
/// for example when building a system-prompt template context that needs
|
||||||
/// context that needs the list of registered tool names. Redundant
|
/// the list of registered tool names. Redundant calls are no-ops.
|
||||||
/// calls are no-ops.
|
|
||||||
///
|
///
|
||||||
/// # Panics
|
/// # Panics
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,8 @@
|
||||||
//!
|
//!
|
||||||
//! 1 リクエストの送信時点での「ある history prefix 長で計測した占有量」を
|
//! 1 リクエストの送信時点での「ある history prefix 長で計測した占有量」を
|
||||||
//! 1 件分にまとめたもの。`UsageEvent` (provider stream イベント) を
|
//! 1 件分にまとめたもの。`UsageEvent` (provider stream イベント) を
|
||||||
//! 受けて呼び出し側 (typically Worker) が組み立て、永続化層
|
//! 受けて呼び出し側が組み立て、永続化層に流したり、token accounting
|
||||||
//! (session-store) に流したり、token accounting (`token_counter`) で
|
//! (`token_counter`) で履歴として参照したりする。
|
||||||
//! 履歴として参照したりする。
|
|
||||||
|
|
||||||
/// LLM リクエスト送信時点での占有量スナップショット。
|
/// LLM リクエスト送信時点での占有量スナップショット。
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,5 @@ toml = { workspace = true }
|
||||||
tracing = { workspace = true }
|
tracing = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
serial_test = "3.4.0"
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -723,7 +723,7 @@ fn resolve_auth_file(auth: &mut Option<AuthRef>, base: &Path) {
|
||||||
|
|
||||||
/// モデル宣言に含まれる `auth.file` が絶対パスであることを検証する。
|
/// モデル宣言に含まれる `auth.file` が絶対パスであることを検証する。
|
||||||
/// ref / scheme / model_id 等の論理的な有効性(ref があるか、inline が
|
/// ref / scheme / model_id 等の論理的な有効性(ref があるか、inline が
|
||||||
/// 揃っているか)の検証はカタログを知る `crates/provider` 側で行う。
|
/// 揃っているか)の検証は [`crate::model_catalog`] で行う。
|
||||||
fn validate_model_paths(model: &ModelManifest, field: &'static str) -> Result<(), ResolveError> {
|
fn validate_model_paths(model: &ModelManifest, field: &'static str) -> Result<(), ResolveError> {
|
||||||
if let Some(AuthRef::ApiKey { file: Some(p), .. }) = &model.auth {
|
if let Some(AuthRef::ApiKey { file: Some(p), .. }) = &model.auth {
|
||||||
ensure_absolute(field, p)?;
|
ensure_absolute(field, p)?;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
mod config;
|
mod config;
|
||||||
pub mod defaults;
|
pub mod defaults;
|
||||||
mod model;
|
mod model;
|
||||||
|
pub mod model_catalog;
|
||||||
pub mod paths;
|
pub mod paths;
|
||||||
pub mod plugin;
|
pub mod plugin;
|
||||||
mod profile;
|
mod profile;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@
|
||||||
//! 直書き)の両方を受け入れるため、すべてのフィールドを `Option` として
|
//! 直書き)の両方を受け入れるため、すべてのフィールドを `Option` として
|
||||||
//! 持つ 1 つの型 [`ModelManifest`] に統合している。実解決(ref をプロバイダ
|
//! 持つ 1 つの型 [`ModelManifest`] に統合している。実解決(ref をプロバイダ
|
||||||
//! カタログ / モデルカタログから引いて `scheme` や `model_id` を埋める)
|
//! カタログ / モデルカタログから引いて `scheme` や `model_id` を埋める)
|
||||||
//! は `crates/provider` の責務で、本モジュールはデータ表現のみを提供する。
|
//! は [`crate::model_catalog`] の責務で、本モジュールはデータ表現のみを提供する。
|
||||||
//!
|
//!
|
||||||
//! 同じ型を partial(カスケード層)と完成形(最終マニフェスト)の両方で
|
//! 同じ型を partial(カスケード層)と完成形(最終マニフェスト)の両方で
|
||||||
//! 使うことで、merge と最終変換の重複を避ける。
|
//! 使うことで、merge と最終変換の重複を避ける。
|
||||||
|
|
@ -24,7 +24,7 @@ pub use llm_engine::llm_client::capability::{ModelCapability, ReasoningControl,
|
||||||
/// - ref + 一部 override: ref で基底を引き、`auth` 等だけ書き換え
|
/// - ref + 一部 override: ref で基底を引き、`auth` 等だけ書き換え
|
||||||
/// - 完全 inline: `ref` を省略して `scheme` / `model_id` / `auth` を直書き
|
/// - 完全 inline: `ref` を省略して `scheme` / `model_id` / `auth` を直書き
|
||||||
///
|
///
|
||||||
/// どの形が有効かの判定は `provider::resolve_model_manifest` が担う。
|
/// どの形が有効かの判定は [`crate::model_catalog::resolve_model_manifest`] が担う。
|
||||||
/// 本クレートは「どこから取るか」を表現するだけで、未設定かどうかを
|
/// 本クレートは「どこから取るか」を表現するだけで、未設定かどうかを
|
||||||
/// 理由にした hard error は出さない。
|
/// 理由にした hard error は出さない。
|
||||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
|
||||||
|
|
@ -98,7 +98,7 @@ pub enum SchemeKind {
|
||||||
/// 認証の参照。
|
/// 認証の参照。
|
||||||
///
|
///
|
||||||
/// 実際のトークン値の解決(local secret store / file 読取、OAuth refresh 等)は
|
/// 実際のトークン値の解決(local secret store / file 読取、OAuth refresh 等)は
|
||||||
/// `crates/provider` で行う。ここはあくまで「どこから取るか」の宣言。
|
/// worker-side factory で行う。ここはあくまで「どこから取るか」の宣言。
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
pub enum AuthRef {
|
pub enum AuthRef {
|
||||||
|
|
@ -113,7 +113,7 @@ pub enum AuthRef {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
file: Option<PathBuf>,
|
file: Option<PathBuf>,
|
||||||
},
|
},
|
||||||
/// ChatGPT OAuth(`~/.codex/auth.json`)。実装は `llm-auth-codex-oauth` チケット
|
/// ChatGPT OAuth(`~/.codex/auth.json`)。
|
||||||
#[serde(rename = "codex_oauth")]
|
#[serde(rename = "codex_oauth")]
|
||||||
CodexOAuth,
|
CodexOAuth,
|
||||||
/// Typed local secret-store reference. The profile resolver preserves this
|
/// Typed local secret-store reference. The profile resolver preserves this
|
||||||
|
|
|
||||||
|
|
@ -4,19 +4,19 @@
|
||||||
//! - builtin モデル: `resources/models/builtin.toml`
|
//! - builtin モデル: `resources/models/builtin.toml`
|
||||||
//! - user override: `<config_dir>/{providers,models}.toml`
|
//! - user override: `<config_dir>/{providers,models}.toml`
|
||||||
//!
|
//!
|
||||||
//! `<config_dir>` の解決は [`manifest::paths::config_dir`] を参照。
|
//! `<config_dir>` の解決は [`crate::paths::config_dir`] を参照。
|
||||||
//! どちらの override も「あれば builtin を置換、無ければ builtin」と
|
//! どちらの override も「あれば builtin を置換、無ければ builtin」と
|
||||||
//! いう一方向の差し替え(マージしない)。providers / models は独立に
|
//! いう一方向の差し替え(マージしない)。providers / models は独立に
|
||||||
//! 読み、片方だけ user override も可。
|
//! 読み、片方だけ user override も可。
|
||||||
//!
|
//!
|
||||||
//! [`resolve_model_manifest`] が `manifest::ModelManifest`(ref / inline
|
//! [`resolve_model_manifest`] が [`crate::ModelManifest`](ref / inline
|
||||||
//! 両形)を最終的な [`ModelConfig`] に解決する単一の入口で、wire 層
|
//! 両形)を最終的な [`ModelConfig`] に解決する単一の入口で、wire 層
|
||||||
//! に渡す前のバリデーションもここで行う。
|
//! に渡す前のバリデーションもここで行う。
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
use crate::{AuthRef, ModelManifest, SchemeKind};
|
||||||
use llm_engine::llm_client::capability::ModelCapability;
|
use llm_engine::llm_client::capability::ModelCapability;
|
||||||
use manifest::{AuthRef, ModelManifest, SchemeKind};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
const BUILTIN_PROVIDERS: &str = include_str!("../../../resources/providers/builtin.toml");
|
const BUILTIN_PROVIDERS: &str = include_str!("../../../resources/providers/builtin.toml");
|
||||||
|
|
@ -178,7 +178,7 @@ fn auth_hint_to_ref(hint: &AuthHint) -> AuthRef {
|
||||||
/// ユーザーが書いた設定が silent に無視されて builtin に戻る挙動は
|
/// ユーザーが書いた設定が silent に無視されて builtin に戻る挙動は
|
||||||
/// 気付きにくいため)。
|
/// 気付きにくいため)。
|
||||||
pub fn load_providers() -> Result<Vec<ProviderEntry>, CatalogError> {
|
pub fn load_providers() -> Result<Vec<ProviderEntry>, 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()
|
&& path.is_file()
|
||||||
{
|
{
|
||||||
return load_providers_from(&path);
|
return load_providers_from(&path);
|
||||||
|
|
@ -211,7 +211,7 @@ pub fn load_providers_from(path: &Path) -> Result<Vec<ProviderEntry>, CatalogErr
|
||||||
|
|
||||||
/// builtin + user override を解決してモデルカタログを返す。
|
/// builtin + user override を解決してモデルカタログを返す。
|
||||||
pub fn load_models() -> Result<Vec<ModelEntry>, CatalogError> {
|
pub fn load_models() -> Result<Vec<ModelEntry>, 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()
|
&& path.is_file()
|
||||||
{
|
{
|
||||||
return load_models_from(&path);
|
return load_models_from(&path);
|
||||||
|
|
@ -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"
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -23,7 +23,6 @@ manifest = { workspace = true }
|
||||||
secrets = { workspace = true }
|
secrets = { workspace = true }
|
||||||
session-store = { workspace = true }
|
session-store = { workspace = true }
|
||||||
fs4 = { workspace = true }
|
fs4 = { workspace = true }
|
||||||
provider = { workspace = true }
|
|
||||||
ticket = { workspace = true }
|
ticket = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
minijinja = "2.19.0"
|
minijinja = "2.19.0"
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use std::io::{self, Write};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::ExitCode;
|
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::Value;
|
||||||
use toml::map::Map;
|
use toml::map::Map;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@ default = []
|
||||||
[dependencies]
|
[dependencies]
|
||||||
async-trait = { workspace = true }
|
async-trait = { workspace = true }
|
||||||
clap = { version = "4.6.0", features = ["derive"] }
|
clap = { version = "4.6.0", features = ["derive"] }
|
||||||
llm-engine = { workspace = true }
|
llm-engine = { workspace = true, features = ["codex"] }
|
||||||
session-store = { workspace = true }
|
session-store = { workspace = true }
|
||||||
|
secrets = { workspace = true }
|
||||||
manifest = { workspace = true }
|
manifest = { workspace = true }
|
||||||
mcp = { workspace = true }
|
mcp = { workspace = true }
|
||||||
protocol = { workspace = true }
|
protocol = { workspace = true }
|
||||||
provider = { workspace = true }
|
|
||||||
client = { workspace = true }
|
client = { workspace = true }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
serde = { workspace = true, features = ["derive"] }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|
@ -46,6 +46,7 @@ futures-util = { version = "0.3", features = ["sink"] }
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
|
serial_test = "3.4.0"
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
wat = "1.241.2"
|
wat = "1.241.2"
|
||||||
yoi-plugin-pdk = { workspace = true }
|
yoi-plugin-pdk = { workspace = true }
|
||||||
|
|
|
||||||
|
|
@ -1526,11 +1526,11 @@ where
|
||||||
let manifest = worker.manifest();
|
let manifest = worker.manifest();
|
||||||
// `build_client` がここに到達する前に同じマニフェストで成功している
|
// `build_client` がここに到達する前に同じマニフェストで成功している
|
||||||
// ため、カタログ解決も必ず通る。念のため失敗時は "unknown" に落とす。
|
// ため、カタログ解決も必ず通る。念のため失敗時は "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
|
let context_window = resolved
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|cfg| cfg.context_window)
|
.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 {
|
let (provider_name, model_id) = match resolved {
|
||||||
Some(cfg) => {
|
Some(cfg) => {
|
||||||
let name = match cfg.scheme {
|
let name = match cfg.scheme {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ pub mod fs_view;
|
||||||
pub mod hook;
|
pub mod hook;
|
||||||
pub(crate) mod in_flight;
|
pub(crate) mod in_flight;
|
||||||
pub mod ipc;
|
pub mod ipc;
|
||||||
|
pub mod model_client;
|
||||||
pub mod prompt;
|
pub mod prompt;
|
||||||
pub mod runtime;
|
pub mod runtime;
|
||||||
pub mod segment_log_sink;
|
pub mod segment_log_sink;
|
||||||
|
|
@ -30,11 +31,11 @@ pub use manifest::{
|
||||||
AuthRef, ModelManifest, SchemeKind, Scope, WorkerManifest, WorkerManifestConfig,
|
AuthRef, ModelManifest, SchemeKind, Scope, WorkerManifest, WorkerManifestConfig,
|
||||||
WorkerMetaConfig,
|
WorkerMetaConfig,
|
||||||
};
|
};
|
||||||
|
pub use model_client::{ProviderError, build_client};
|
||||||
pub use prompt::catalog::{CatalogError, PromptCatalog, WorkerPrompt};
|
pub use prompt::catalog::{CatalogError, PromptCatalog, WorkerPrompt};
|
||||||
pub use prompt::loader::PromptLoader;
|
pub use prompt::loader::PromptLoader;
|
||||||
pub use prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTemplate};
|
pub use prompt::system::{SystemPromptContext, SystemPromptError, SystemPromptTemplate};
|
||||||
pub use protocol::{ErrorCode, Event, Method, TurnResult, WorkerStatus};
|
pub use protocol::{ErrorCode, Event, Method, TurnResult, WorkerStatus};
|
||||||
pub use provider::{ProviderError, build_client};
|
|
||||||
pub use runtime::dir::RuntimeDir;
|
pub use runtime::dir::RuntimeDir;
|
||||||
pub use segment_log_sink::SegmentLogSink;
|
pub use segment_log_sink::SegmentLogSink;
|
||||||
pub use shared_state::WorkerSharedState;
|
pub use shared_state::WorkerSharedState;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
//! Worker マニフェストの [`ModelManifest`] を [`Box<dyn LlmClient>`]
|
//! [`ModelManifest`] を [`Box<dyn LlmClient>`] に落とす worker-side factory。
|
||||||
//! に落とすファクトリ。
|
|
||||||
//!
|
//!
|
||||||
//! 段階:
|
//! 段階:
|
||||||
//! 1. `ModelManifest` を [`catalog::resolve_model_manifest`] で
|
//! 1. `ModelManifest` を [`catalog::resolve_model_manifest`] で
|
||||||
|
|
@ -11,11 +10,7 @@
|
||||||
//! `catalog::resolve_model_manifest` が [`ModelConfig`] に詰め込む)
|
//! `catalog::resolve_model_manifest` が [`ModelConfig`] に詰め込む)
|
||||||
//!
|
//!
|
||||||
//! llm-engine は低レベル基盤に留める方針なので、高レベル側で必要に
|
//! llm-engine は低レベル基盤に留める方針なので、高レベル側で必要に
|
||||||
//! なる認証ストア解決(Codex OAuth の `~/.codex/auth.json` 読取等)は
|
//! なる認証ストア解決と secret store 解決は worker 側で行う。
|
||||||
//! このクレートに追加する。
|
|
||||||
|
|
||||||
pub mod catalog;
|
|
||||||
pub mod codex_oauth;
|
|
||||||
|
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
|
@ -26,13 +21,14 @@ use llm_engine::llm_client::{
|
||||||
Scheme, anthropic::AnthropicScheme, gemini::GeminiScheme, openai_chat::OpenAIScheme,
|
Scheme, anthropic::AnthropicScheme, gemini::GeminiScheme, openai_chat::OpenAIScheme,
|
||||||
openai_responses::OpenAIResponsesScheme,
|
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};
|
use secrets::{SecretStore, SecretValue};
|
||||||
|
|
||||||
pub use catalog::{ModelConfig, ResolveError as CatalogResolveError};
|
pub use manifest::model_catalog::{ModelConfig, ResolveError as CatalogResolveError};
|
||||||
|
|
||||||
/// プロバイダ構築時のエラー。
|
/// プロバイダ構築時のエラー。
|
||||||
#[derive(Debug, thiserror::Error)]
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
|
@ -111,7 +107,7 @@ fn resolve_auth_with_resolver(
|
||||||
Err(ProviderError::ApiKeyMissing { scheme })
|
Err(ProviderError::ApiKeyMissing { scheme })
|
||||||
}
|
}
|
||||||
AuthRef::CodexOAuth => {
|
AuthRef::CodexOAuth => {
|
||||||
let provider = codex_oauth::CodexAuthProvider::from_default_home()
|
let provider = CodexAuthProvider::from_default_home()
|
||||||
.map_err(|e| ProviderError::Config(e.to_string()))?;
|
.map_err(|e| ProviderError::Config(e.to_string()))?;
|
||||||
Ok(ResolvedAuth::Custom(Arc::new(provider)))
|
Ok(ResolvedAuth::Custom(Arc::new(provider)))
|
||||||
}
|
}
|
||||||
|
|
@ -144,6 +140,7 @@ fn build_transport<S: Scheme>(
|
||||||
scheme: S,
|
scheme: S,
|
||||||
config: &ModelConfig,
|
config: &ModelConfig,
|
||||||
resolved: ResolvedAuth,
|
resolved: ResolvedAuth,
|
||||||
|
policy: TransportPolicy,
|
||||||
) -> Result<Box<dyn LlmClient>, ProviderError> {
|
) -> Result<Box<dyn LlmClient>, ProviderError> {
|
||||||
if !resolved.matches(scheme.required_auth()) {
|
if !resolved.matches(scheme.required_auth()) {
|
||||||
return Err(ProviderError::AuthMismatch {
|
return Err(ProviderError::AuthMismatch {
|
||||||
|
|
@ -160,21 +157,39 @@ fn build_transport<S: Scheme>(
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| scheme.default_capability());
|
.unwrap_or_else(|| scheme.default_capability());
|
||||||
let base_url = effective_base_url(&scheme, config);
|
let base_url = effective_base_url(&scheme, config);
|
||||||
Ok(Box::new(HttpTransport::new(
|
Ok(Box::new(
|
||||||
scheme,
|
HttpTransport::new(
|
||||||
config.model_id.clone(),
|
scheme,
|
||||||
base_url,
|
config.model_id.clone(),
|
||||||
resolved,
|
base_url,
|
||||||
capability,
|
resolved,
|
||||||
)))
|
capability,
|
||||||
|
)
|
||||||
|
.with_transport_policy(policy),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_from_config(config: &ModelConfig) -> Result<Box<dyn LlmClient>, ProviderError> {
|
fn build_from_config(config: &ModelConfig) -> Result<Box<dyn LlmClient>, ProviderError> {
|
||||||
let resolved = resolve_auth(config.scheme, &config.auth)?;
|
let resolved = resolve_auth(config.scheme, &config.auth)?;
|
||||||
match config.scheme {
|
match config.scheme {
|
||||||
SchemeKind::Anthropic => build_transport(AnthropicScheme::new(), config, resolved),
|
SchemeKind::Anthropic => build_transport(
|
||||||
SchemeKind::OpenaiChat => build_transport(OpenAIScheme::new(), config, resolved),
|
AnthropicScheme::new(),
|
||||||
SchemeKind::Gemini => build_transport(GeminiScheme::new(), config, resolved),
|
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 => {
|
SchemeKind::OpenaiResponses => {
|
||||||
// ChatGPT backend (codex-oauth) は `max_output_tokens` /
|
// ChatGPT backend (codex-oauth) は `max_output_tokens` /
|
||||||
// `temperature` / `top_p` を 400 で弾くため、その経路では
|
// `temperature` / `top_p` を 400 で弾くため、その経路では
|
||||||
|
|
@ -183,7 +198,12 @@ fn build_from_config(config: &ModelConfig) -> Result<Box<dyn LlmClient>, Provide
|
||||||
let scheme = OpenAIResponsesScheme::new()
|
let scheme = OpenAIResponsesScheme::new()
|
||||||
.with_send_max_output_tokens(send_to_official)
|
.with_send_max_output_tokens(send_to_official)
|
||||||
.with_send_sampling_params(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 super::*;
|
||||||
use serial_test::serial;
|
use serial_test::serial;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
struct TestSecrets(std::collections::BTreeMap<String, String>);
|
struct TestSecrets(std::collections::BTreeMap<String, String>);
|
||||||
|
|
||||||
|
struct ConfigDirGuard {
|
||||||
|
prev: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
impl SecretResolver for TestSecrets {
|
||||||
fn get_secret(&self, id: &str) -> Result<SecretValue, secrets::Error> {
|
fn get_secret(&self, id: &str) -> Result<SecretValue, secrets::Error> {
|
||||||
self.0
|
self.0
|
||||||
|
|
@ -311,7 +355,11 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[serial]
|
||||||
fn ref_manifest_builds_client() {
|
fn ref_manifest_builds_client() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let _guard = ConfigDirGuard::new(dir.path());
|
||||||
|
|
||||||
// Ollama は AuthRef::None で構築できる end-to-end path。
|
// Ollama は AuthRef::None で構築できる end-to-end path。
|
||||||
let manifest = ModelManifest {
|
let manifest = ModelManifest {
|
||||||
ref_: Some("ollama-local/llama3.1".into()),
|
ref_: Some("ollama-local/llama3.1".into()),
|
||||||
|
|
@ -326,7 +374,11 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[serial]
|
||||||
fn inline_manifest_builds_client() {
|
fn inline_manifest_builds_client() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let _guard = ConfigDirGuard::new(dir.path());
|
||||||
|
|
||||||
// Form C: 完全直書き。Ollama 相当を AuthRef::None で構築。
|
// Form C: 完全直書き。Ollama 相当を AuthRef::None で構築。
|
||||||
let manifest = ModelManifest {
|
let manifest = ModelManifest {
|
||||||
scheme: Some(SchemeKind::Anthropic),
|
scheme: Some(SchemeKind::Anthropic),
|
||||||
|
|
@ -3132,7 +3132,7 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||||
fn build_compactor_client(&self) -> Result<Box<dyn LlmClient>, WorkerError> {
|
fn build_compactor_client(&self) -> Result<Box<dyn LlmClient>, WorkerError> {
|
||||||
if let Some(ref compaction) = self.manifest.compaction {
|
if let Some(ref compaction) = self.manifest.compaction {
|
||||||
if let Some(ref model_config) = compaction.model {
|
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);
|
return Ok(client);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3149,7 +3149,7 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||||
memory_cfg: &manifest::MemoryConfig,
|
memory_cfg: &manifest::MemoryConfig,
|
||||||
) -> Result<Box<dyn LlmClient>, WorkerError> {
|
) -> Result<Box<dyn LlmClient>, WorkerError> {
|
||||||
if let Some(ref m) = memory_cfg.extract_model {
|
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);
|
return Ok(client);
|
||||||
}
|
}
|
||||||
let worker = self.engine.as_ref().expect("worker taken during run");
|
let worker = self.engine.as_ref().expect("worker taken during run");
|
||||||
|
|
@ -3586,7 +3586,7 @@ impl<C: LlmClient, St: Store> Worker<C, St> {
|
||||||
memory_cfg: &manifest::MemoryConfig,
|
memory_cfg: &manifest::MemoryConfig,
|
||||||
) -> Result<Box<dyn LlmClient>, WorkerError> {
|
) -> Result<Box<dyn LlmClient>, WorkerError> {
|
||||||
if let Some(ref m) = memory_cfg.consolidation_model {
|
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);
|
return Ok(client);
|
||||||
}
|
}
|
||||||
let worker = self.engine.as_ref().expect("worker taken during run");
|
let worker = self.engine.as_ref().expect("worker taken during run");
|
||||||
|
|
@ -5154,7 +5154,7 @@ pub enum WorkerError {
|
||||||
ManifestResolve(#[source] ResolveError),
|
ManifestResolve(#[source] ResolveError),
|
||||||
|
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Provider(#[from] provider::ProviderError),
|
Provider(#[from] crate::model_client::ProviderError),
|
||||||
|
|
||||||
#[error("compaction thrash: context still exceeds threshold immediately after compact")]
|
#[error("compaction thrash: context still exceeds threshold immediately after compact")]
|
||||||
CompactThrash,
|
CompactThrash,
|
||||||
|
|
@ -5383,7 +5383,7 @@ fn prepare_worker_common_from_scope(
|
||||||
let delegation_scope =
|
let delegation_scope =
|
||||||
DelegationScope::from_config(&manifest.delegation_scope).map_err(WorkerError::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 prompts = PromptCatalog::load(loader, manifest.worker.prompt_pack.as_deref())?;
|
||||||
let memory_layout = manifest.memory.as_ref().and_then(|mem| {
|
let memory_layout = manifest.memory.as_ref().and_then(|mem| {
|
||||||
filesystem_authority
|
filesystem_authority
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ name = "example-agent"
|
||||||
# (b) `ref` + 一部 override — auth など個別差し替え
|
# (b) `ref` + 一部 override — auth など個別差し替え
|
||||||
# (c) `scheme` + `model_id` 直書き — カタログを使わない inline 指定
|
# (c) `scheme` + `model_id` 直書き — カタログを使わない inline 指定
|
||||||
# (b) / (c) では `ref` 未指定なら `scheme` / `model_id` / `auth` が必須。
|
# (b) / (c) では `ref` 未指定なら `scheme` / `model_id` / `auth` が必須。
|
||||||
# (実際の必須判定は `crates/provider` の resolve 側で行う)
|
# (実際の必須判定は manifest::model_catalog の resolve 側で行う)
|
||||||
[model]
|
[model]
|
||||||
|
|
||||||
# 任意。形式: "<provider_id>/<model_id_in_ref>"。
|
# 任意。形式: "<provider_id>/<model_id_in_ref>"。
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user