llm-model-configの実装

This commit is contained in:
2026-04-19 23:32:14 +09:00
parent e1d672e9c0
commit 230936274b
50 changed files with 1718 additions and 1175 deletions
+3 -3
View File
@@ -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."
+3 -3
View File
@@ -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."
+8 -8
View File
@@ -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
View File
@@ -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}"
+1 -1
View File
@@ -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;
+6 -6
View File
@@ -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
View File
@@ -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")));
}
}
+3 -3
View File
@@ -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 -11
View File
@@ -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