llm-model-configの実装
This commit is contained in:
@@ -22,9 +22,9 @@ fn manifest_toml(pwd: &std::path::Path) -> String {
|
||||
name = "hello-pod"
|
||||
pwd = "{pwd}"
|
||||
|
||||
[provider]
|
||||
kind = "anthropic"
|
||||
model = "claude-sonnet-4-20250514"
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "claude-sonnet-4-20250514"
|
||||
|
||||
[worker]
|
||||
system_prompt = "You are a concise assistant. Reply in one or two sentences."
|
||||
|
||||
@@ -16,9 +16,9 @@ fn manifest_toml(pwd: &std::path::Path) -> String {
|
||||
name = "protocol-demo"
|
||||
pwd = "{pwd}"
|
||||
|
||||
[provider]
|
||||
kind = "anthropic"
|
||||
model = "claude-sonnet-4-20250514"
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "claude-sonnet-4-20250514"
|
||||
|
||||
[worker]
|
||||
system_prompt = "You are a concise assistant. Reply in one or two sentences."
|
||||
|
||||
@@ -113,7 +113,7 @@ impl PodController {
|
||||
let scope_for_tools = pod.scope().clone();
|
||||
let pwd_for_tools = pod.pwd().to_path_buf();
|
||||
let spawner_name = pod.manifest().pod.name.clone();
|
||||
let spawner_provider = pod.manifest().provider.clone();
|
||||
let spawner_model = pod.manifest().model.clone();
|
||||
|
||||
// Parent callback socket (this Pod's own parent, used for
|
||||
// `PodEvent` upward reports). `None` for top-level Pods.
|
||||
@@ -230,7 +230,7 @@ impl PodController {
|
||||
pwd_for_tools,
|
||||
spawned_registry.clone(),
|
||||
self_parent_socket.clone(),
|
||||
spawner_provider.clone(),
|
||||
spawner_model.clone(),
|
||||
));
|
||||
worker.register_tool(send_to_pod_tool(spawned_registry.clone()));
|
||||
worker.register_tool(read_pod_output_tool(spawned_registry.clone()));
|
||||
@@ -663,11 +663,11 @@ where
|
||||
St: Store,
|
||||
{
|
||||
let manifest = pod.manifest();
|
||||
let provider = match manifest.provider.kind {
|
||||
manifest::ProviderKind::Anthropic => "anthropic",
|
||||
manifest::ProviderKind::Openai => "openai",
|
||||
manifest::ProviderKind::Gemini => "gemini",
|
||||
manifest::ProviderKind::Ollama => "ollama",
|
||||
let provider = match manifest.model.scheme {
|
||||
manifest::SchemeKind::Anthropic => "anthropic",
|
||||
manifest::SchemeKind::OpenaiChat => "openai_chat",
|
||||
manifest::SchemeKind::OpenaiResponses => "openai_responses",
|
||||
manifest::SchemeKind::Gemini => "gemini",
|
||||
};
|
||||
// The tool list mirrors what `spawn()` registers on the Worker:
|
||||
// builtin filesystem tools plus the pod-orchestration tools.
|
||||
@@ -689,7 +689,7 @@ where
|
||||
pod_name: manifest.pod.name.clone(),
|
||||
cwd: pod.pwd().display().to_string(),
|
||||
provider: provider.into(),
|
||||
model: manifest.provider.model.clone(),
|
||||
model: manifest.model.model_id.clone(),
|
||||
scope_summary: pod.scope().summary(),
|
||||
tools: tool_names,
|
||||
}
|
||||
|
||||
+34
-34
@@ -317,9 +317,9 @@ mod tests {
|
||||
[pod]
|
||||
name = "solo"
|
||||
|
||||
[provider]
|
||||
kind = "anthropic"
|
||||
model = "claude-sonnet-4-20250514"
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "claude-sonnet-4-20250514"
|
||||
|
||||
[[scope.allow]]
|
||||
target = "{pwd}"
|
||||
@@ -342,9 +342,9 @@ permission = "write"
|
||||
let pwd = tmp.path().canonicalize().unwrap();
|
||||
let user_cfg = PodManifestConfig::from_toml(&format!(
|
||||
r#"
|
||||
[provider]
|
||||
kind = "anthropic"
|
||||
model = "user-model"
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "user-model"
|
||||
|
||||
[[scope.allow]]
|
||||
target = "{pwd}"
|
||||
@@ -355,8 +355,8 @@ permission = "read"
|
||||
.unwrap();
|
||||
let project_cfg = PodManifestConfig::from_toml(&format!(
|
||||
r#"
|
||||
[provider]
|
||||
model = "project-model"
|
||||
[model]
|
||||
model_id = "project-model"
|
||||
|
||||
[[scope.allow]]
|
||||
target = "{pwd}"
|
||||
@@ -387,7 +387,7 @@ name = "overlay-name"
|
||||
// overlay layer so later calls win. This also exercises the
|
||||
// scope union across layers (two allow rules).
|
||||
assert_eq!(manifest.pod.name, "overlay-name");
|
||||
assert_eq!(manifest.provider.model, "project-model");
|
||||
assert_eq!(manifest.model.model_id, "project-model");
|
||||
assert_eq!(manifest.scope.allow.len(), 2);
|
||||
}
|
||||
|
||||
@@ -406,9 +406,9 @@ name = "overlay-name"
|
||||
[pod]
|
||||
name = "from-user"
|
||||
|
||||
[provider]
|
||||
kind = "anthropic"
|
||||
model = "user-model"
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "user-model"
|
||||
|
||||
[[scope.allow]]
|
||||
target = "{pwd}"
|
||||
@@ -423,8 +423,8 @@ permission = "write"
|
||||
write(
|
||||
&project_manifest,
|
||||
r#"
|
||||
[provider]
|
||||
model = "project-model"
|
||||
[model]
|
||||
model_id = "project-model"
|
||||
"#,
|
||||
);
|
||||
|
||||
@@ -436,8 +436,8 @@ model = "project-model"
|
||||
.resolve()
|
||||
.unwrap();
|
||||
|
||||
// project layer overrides user layer on provider.model
|
||||
assert_eq!(manifest.provider.model, "project-model");
|
||||
// project layer overrides user layer on model.model_id
|
||||
assert_eq!(manifest.model.model_id, "project-model");
|
||||
// user layer provides the rest
|
||||
assert_eq!(manifest.pod.name, "from-user");
|
||||
}
|
||||
@@ -454,9 +454,9 @@ model = "project-model"
|
||||
[pod]
|
||||
name = "walked-up"
|
||||
|
||||
[provider]
|
||||
kind = "anthropic"
|
||||
model = "claude-sonnet-4-20250514"
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "claude-sonnet-4-20250514"
|
||||
|
||||
[[scope.allow]]
|
||||
target = "{root}"
|
||||
@@ -487,9 +487,9 @@ permission = "write"
|
||||
[pod]
|
||||
name = "standalone"
|
||||
|
||||
[provider]
|
||||
kind = "anthropic"
|
||||
model = "m"
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "m"
|
||||
|
||||
[[scope.allow]]
|
||||
target = "{pwd}"
|
||||
@@ -529,9 +529,9 @@ permission = "write"
|
||||
[pod]
|
||||
name = "rel-user"
|
||||
|
||||
[provider]
|
||||
kind = "anthropic"
|
||||
model = "m"
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "m"
|
||||
|
||||
[[scope.allow]]
|
||||
target = "./workspace"
|
||||
@@ -565,9 +565,9 @@ permission = "write"
|
||||
[pod]
|
||||
name = "rel-project"
|
||||
|
||||
[provider]
|
||||
kind = "anthropic"
|
||||
model = "m"
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "m"
|
||||
|
||||
[[scope.allow]]
|
||||
target = "."
|
||||
@@ -604,9 +604,9 @@ permission = "write"
|
||||
[pod]
|
||||
name = "factory-pod"
|
||||
|
||||
[provider]
|
||||
kind = "anthropic"
|
||||
model = "m"
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "m"
|
||||
|
||||
[[scope.allow]]
|
||||
target = "{root}"
|
||||
@@ -661,9 +661,9 @@ permission = "write"
|
||||
// pod.name missing — resolver must reject.
|
||||
let overlay = format!(
|
||||
r#"
|
||||
[provider]
|
||||
kind = "anthropic"
|
||||
model = "m"
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "m"
|
||||
|
||||
[[scope.allow]]
|
||||
target = "{pwd}"
|
||||
|
||||
@@ -31,7 +31,7 @@ pub use factory::{FactoryError, PodFactory};
|
||||
pub use notifier::Notifier;
|
||||
pub use hook::{Hook, HookEventKind, HookRegistryBuilder};
|
||||
pub use manifest::{
|
||||
PodManifest, PodManifestConfig, PodMetaConfig, ProviderConfig, ProviderKind, Scope,
|
||||
AuthRef, ModelConfig, PodManifest, PodManifestConfig, PodMetaConfig, Scope, SchemeKind,
|
||||
};
|
||||
pub use pod::{Pod, PodError, PodRunResult, apply_worker_manifest};
|
||||
pub use prompt_loader::PromptLoader;
|
||||
|
||||
@@ -818,7 +818,7 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
/// `[summary, ...recent_turns]` and creating a new session.
|
||||
///
|
||||
/// The summary Worker uses:
|
||||
/// - `compaction.provider` from the manifest if configured, or
|
||||
/// - `compaction.model` from the manifest if configured, or
|
||||
/// - a clone of the main LlmClient via `clone_boxed()`.
|
||||
///
|
||||
/// Returns the new session ID.
|
||||
@@ -1056,12 +1056,12 @@ impl<C: LlmClient, St: Store> Pod<C, St> {
|
||||
|
||||
/// Build the LlmClient for the compactor Worker.
|
||||
///
|
||||
/// Uses `compaction.provider` from manifest if set, otherwise clones
|
||||
/// Uses `compaction.model` from manifest if set, otherwise clones
|
||||
/// the main client.
|
||||
fn build_compactor_client(&self) -> Result<Box<dyn LlmClient>, PodError> {
|
||||
if let Some(ref compaction) = self.manifest.compaction {
|
||||
if let Some(ref provider_config) = compaction.provider {
|
||||
let client = provider::build_client(provider_config)?;
|
||||
if let Some(ref model_config) = compaction.model {
|
||||
let client = provider::build_client(model_config)?;
|
||||
return Ok(client);
|
||||
}
|
||||
}
|
||||
@@ -1109,7 +1109,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
|
||||
scope.allow_rules(),
|
||||
)?;
|
||||
|
||||
let client = provider::build_client(&manifest.provider)?;
|
||||
let client = provider::build_client(&manifest.model)?;
|
||||
let mut worker = Worker::new(client);
|
||||
apply_worker_manifest(&mut worker, &manifest.worker);
|
||||
|
||||
@@ -1176,7 +1176,7 @@ impl<St: Store> Pod<Box<dyn LlmClient>, St> {
|
||||
let scope_allocation =
|
||||
scope_lock::adopt_allocation(manifest.pod.name.clone(), std::process::id())?;
|
||||
|
||||
let client = provider::build_client(&manifest.provider)?;
|
||||
let client = provider::build_client(&manifest.model)?;
|
||||
let mut worker = Worker::new(client);
|
||||
apply_worker_manifest(&mut worker, &manifest.worker);
|
||||
|
||||
|
||||
+32
-31
@@ -14,8 +14,8 @@ use std::time::Duration;
|
||||
use async_trait::async_trait;
|
||||
use llm_worker::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||
use manifest::{
|
||||
Permission, PodManifestConfig, PodMetaConfig, ProviderConfig, ProviderConfigPartial,
|
||||
ScopeConfig, ScopeRule, WorkerManifestConfig,
|
||||
ModelConfig, ModelConfigPartial, Permission, PodManifestConfig, PodMetaConfig, ScopeConfig,
|
||||
ScopeRule, WorkerManifestConfig,
|
||||
};
|
||||
use protocol::Method;
|
||||
use protocol::stream::JsonLineWriter;
|
||||
@@ -118,7 +118,7 @@ pub struct SpawnPodTool {
|
||||
/// Pod's overlay TOML so the child does not need its own provider
|
||||
/// configuration in the manifest cascade. Per-spawn override is
|
||||
/// out of scope here (see `tickets/spawn-inherit-provider.md`).
|
||||
spawner_provider: ProviderConfig,
|
||||
spawner_model: ModelConfig,
|
||||
}
|
||||
|
||||
impl SpawnPodTool {
|
||||
@@ -129,7 +129,7 @@ impl SpawnPodTool {
|
||||
spawner_pwd: PathBuf,
|
||||
registry: Arc<SpawnedPodRegistry>,
|
||||
parent_socket: Option<PathBuf>,
|
||||
spawner_provider: ProviderConfig,
|
||||
spawner_model: ModelConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
spawner_name,
|
||||
@@ -138,7 +138,7 @@ impl SpawnPodTool {
|
||||
spawner_pwd,
|
||||
registry,
|
||||
parent_socket,
|
||||
spawner_provider,
|
||||
spawner_model,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -196,7 +196,7 @@ impl Tool for SpawnPodTool {
|
||||
&input.name,
|
||||
&instruction,
|
||||
&scope_allow,
|
||||
&self.spawner_provider,
|
||||
&self.spawner_model,
|
||||
) {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
@@ -350,17 +350,17 @@ fn build_overlay_toml(
|
||||
name: &str,
|
||||
instruction: &str,
|
||||
scope_allow: &[ScopeRule],
|
||||
provider: &ProviderConfig,
|
||||
model: &ModelConfig,
|
||||
) -> Result<String, toml::ser::Error> {
|
||||
let overlay = PodManifestConfig {
|
||||
pod: PodMetaConfig {
|
||||
name: Some(name.to_string()),
|
||||
},
|
||||
provider: ProviderConfigPartial {
|
||||
kind: Some(provider.kind),
|
||||
model: Some(provider.model.clone()),
|
||||
api_key_file: provider.api_key_file.clone(),
|
||||
base_url: provider.base_url.clone(),
|
||||
model: ModelConfigPartial {
|
||||
scheme: Some(model.scheme),
|
||||
base_url: model.base_url.clone(),
|
||||
model_id: Some(model.model_id.clone()),
|
||||
auth: Some(model.auth.clone()),
|
||||
},
|
||||
worker: WorkerManifestConfig {
|
||||
instruction: Some(instruction.to_string()),
|
||||
@@ -458,7 +458,7 @@ pub fn spawn_pod_tool(
|
||||
spawner_pwd: PathBuf,
|
||||
registry: Arc<SpawnedPodRegistry>,
|
||||
parent_socket: Option<PathBuf>,
|
||||
spawner_provider: ProviderConfig,
|
||||
spawner_model: ModelConfig,
|
||||
) -> ToolDefinition {
|
||||
Arc::new(move || {
|
||||
let schema = schemars::schema_for!(SpawnPodInput);
|
||||
@@ -473,7 +473,7 @@ pub fn spawn_pod_tool(
|
||||
spawner_pwd.clone(),
|
||||
registry.clone(),
|
||||
parent_socket.clone(),
|
||||
spawner_provider.clone(),
|
||||
spawner_model.clone(),
|
||||
));
|
||||
(meta, tool)
|
||||
})
|
||||
@@ -482,29 +482,30 @@ pub fn spawn_pod_tool(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use manifest::ProviderKind;
|
||||
use manifest::{AuthRef, SchemeKind};
|
||||
|
||||
#[test]
|
||||
fn overlay_inherits_spawner_provider() {
|
||||
let provider = ProviderConfig {
|
||||
kind: ProviderKind::Anthropic,
|
||||
model: "claude-sonnet-4".into(),
|
||||
api_key_file: Some(PathBuf::from("/etc/keys/anthropic")),
|
||||
fn overlay_inherits_spawner_model() {
|
||||
let model = ModelConfig {
|
||||
scheme: SchemeKind::Anthropic,
|
||||
base_url: Some("https://example.test".into()),
|
||||
model_id: "claude-sonnet-4".into(),
|
||||
auth: AuthRef::ApiKey {
|
||||
env: None,
|
||||
file: Some(PathBuf::from("/etc/keys/anthropic")),
|
||||
},
|
||||
};
|
||||
|
||||
let toml_str = build_overlay_toml("child", "$insomnia/default", &[], &provider).unwrap();
|
||||
let toml_str = build_overlay_toml("child", "$insomnia/default", &[], &model).unwrap();
|
||||
let parsed = PodManifestConfig::from_toml(&toml_str).unwrap();
|
||||
|
||||
assert_eq!(parsed.provider.kind, Some(ProviderKind::Anthropic));
|
||||
assert_eq!(parsed.provider.model.as_deref(), Some("claude-sonnet-4"));
|
||||
assert_eq!(
|
||||
parsed.provider.api_key_file.as_deref(),
|
||||
Some(Path::new("/etc/keys/anthropic"))
|
||||
);
|
||||
assert_eq!(
|
||||
parsed.provider.base_url.as_deref(),
|
||||
Some("https://example.test")
|
||||
);
|
||||
assert_eq!(parsed.model.scheme, Some(SchemeKind::Anthropic));
|
||||
assert_eq!(parsed.model.model_id.as_deref(), Some("claude-sonnet-4"));
|
||||
assert_eq!(parsed.model.base_url.as_deref(), Some("https://example.test"));
|
||||
let file = match parsed.model.auth {
|
||||
Some(AuthRef::ApiKey { file, .. }) => file,
|
||||
_ => panic!("expected ApiKey"),
|
||||
};
|
||||
assert_eq!(file.as_deref(), Some(Path::new("/etc/keys/anthropic")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,9 +110,9 @@ const MANIFEST_TOML: &str = r#"
|
||||
name = "test-pod"
|
||||
pwd = "./"
|
||||
|
||||
[provider]
|
||||
kind = "anthropic"
|
||||
model = "test-model"
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "test-model"
|
||||
|
||||
[worker]
|
||||
max_tokens = 100
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::path::{Path, PathBuf};
|
||||
use std::sync::{LazyLock, Mutex};
|
||||
|
||||
use llm_worker::tool::{ToolError, ToolOutput};
|
||||
use manifest::{Permission, ProviderConfig, ProviderKind, ScopeRule};
|
||||
use manifest::{AuthRef, ModelConfig, Permission, SchemeKind, ScopeRule};
|
||||
use pod::runtime_dir::{RuntimeDir, SpawnedPodRecord};
|
||||
use pod::scope_lock::{self, LockFileGuard};
|
||||
use pod::spawn_pod::spawn_pod_tool;
|
||||
@@ -132,15 +132,15 @@ fn which_true() -> String {
|
||||
"/bin/true".into()
|
||||
}
|
||||
|
||||
/// Tests don't exercise the provider — they intercept the spawned
|
||||
/// Tests don't exercise the model — they intercept the spawned
|
||||
/// child via a mock socket — but `spawn_pod_tool` needs a value to
|
||||
/// embed in the overlay TOML. Any well-formed `ProviderConfig` works.
|
||||
fn dummy_provider() -> ProviderConfig {
|
||||
ProviderConfig {
|
||||
kind: ProviderKind::Anthropic,
|
||||
model: "claude-test".into(),
|
||||
api_key_file: None,
|
||||
/// embed in the overlay TOML. Any well-formed `ModelConfig` works.
|
||||
fn dummy_model() -> ModelConfig {
|
||||
ModelConfig {
|
||||
scheme: SchemeKind::Anthropic,
|
||||
base_url: None,
|
||||
model_id: "claude-test".into(),
|
||||
auth: AuthRef::None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,7 +171,7 @@ async fn spawn_pod_delegates_scope_and_sends_run() {
|
||||
allow_root.path().to_path_buf(),
|
||||
registry,
|
||||
None,
|
||||
dummy_provider(),
|
||||
dummy_model(),
|
||||
);
|
||||
let (_meta, tool) = def();
|
||||
|
||||
@@ -234,7 +234,7 @@ async fn spawn_pod_rejects_scope_outside_spawner() {
|
||||
allow_root.path().to_path_buf(),
|
||||
registry,
|
||||
None,
|
||||
dummy_provider(),
|
||||
dummy_model(),
|
||||
);
|
||||
let (_meta, tool) = def();
|
||||
|
||||
@@ -293,7 +293,7 @@ async fn spawn_pod_rolls_back_reservation_when_socket_never_appears() {
|
||||
allow_root.path().to_path_buf(),
|
||||
registry,
|
||||
None,
|
||||
dummy_provider(),
|
||||
dummy_model(),
|
||||
);
|
||||
let (_meta, tool) = def();
|
||||
|
||||
|
||||
@@ -79,9 +79,9 @@ const MINIMAL_MANIFEST_TOML: &str = r#"
|
||||
name = "test-pod"
|
||||
pwd = "./"
|
||||
|
||||
[provider]
|
||||
kind = "anthropic"
|
||||
model = "test-model"
|
||||
[model]
|
||||
scheme = "anthropic"
|
||||
model_id = "test-model"
|
||||
|
||||
[worker]
|
||||
max_tokens = 100
|
||||
|
||||
Reference in New Issue
Block a user