modelsとprovidersをカタログ化
This commit is contained in:
@@ -13,7 +13,7 @@ use std::path::{Path, PathBuf};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::defaults;
|
||||
use crate::model::{AuthRef, ModelConfig, SchemeKind};
|
||||
use crate::model::{AuthRef, ModelManifest};
|
||||
use crate::{CompactionConfig, PodManifest, PodMeta, ScopeConfig, ToolOutputLimits, WorkerManifest};
|
||||
|
||||
/// Partial-form Pod manifest. Every field is optional; one or more
|
||||
@@ -23,8 +23,12 @@ use crate::{CompactionConfig, PodManifest, PodMeta, ScopeConfig, ToolOutputLimit
|
||||
pub struct PodManifestConfig {
|
||||
#[serde(default)]
|
||||
pub pod: PodMetaConfig,
|
||||
/// `[model]` セクションは partial でも完成形でも同じ
|
||||
/// [`ModelManifest`] を使う。ref / inline の両形を受け入れるための
|
||||
/// 全 Optional 構造なので、カスケード層と最終マニフェストで型を
|
||||
/// 分ける必要がない。
|
||||
#[serde(default)]
|
||||
pub model: ModelConfigPartial,
|
||||
pub model: ModelManifest,
|
||||
#[serde(default)]
|
||||
pub worker: WorkerManifestConfig,
|
||||
#[serde(default)]
|
||||
@@ -44,21 +48,6 @@ pub struct PodMetaConfig {
|
||||
pub prompt_pack: Option<PathBuf>,
|
||||
}
|
||||
|
||||
/// Partial-form of [`ModelConfig`]. カスケード層で個別に与えられる。
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct ModelConfigPartial {
|
||||
#[serde(default)]
|
||||
pub scheme: Option<SchemeKind>,
|
||||
#[serde(default)]
|
||||
pub base_url: Option<String>,
|
||||
#[serde(default)]
|
||||
pub model_id: Option<String>,
|
||||
#[serde(default)]
|
||||
pub auth: Option<AuthRef>,
|
||||
#[serde(default)]
|
||||
pub capability: Option<crate::model::ModelCapability>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct WorkerManifestConfig {
|
||||
#[serde(default)]
|
||||
@@ -98,7 +87,7 @@ pub struct CompactionConfigPartial {
|
||||
#[serde(default)]
|
||||
pub compact_worker_max_input_tokens: Option<u64>,
|
||||
#[serde(default)]
|
||||
pub model: Option<ModelConfigPartial>,
|
||||
pub model: Option<ModelManifest>,
|
||||
}
|
||||
|
||||
/// Errors raised when converting a [`PodManifestConfig`] to a validated
|
||||
@@ -209,18 +198,6 @@ impl PodMetaConfig {
|
||||
}
|
||||
}
|
||||
|
||||
impl ModelConfigPartial {
|
||||
fn merge(self, upper: Self) -> Self {
|
||||
Self {
|
||||
scheme: upper.scheme.or(self.scheme),
|
||||
base_url: upper.base_url.or(self.base_url),
|
||||
model_id: upper.model_id.or(self.model_id),
|
||||
auth: upper.auth.or(self.auth),
|
||||
capability: upper.capability.or(self.capability),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl WorkerManifestConfig {
|
||||
fn merge(self, upper: Self) -> Self {
|
||||
Self {
|
||||
@@ -262,7 +239,7 @@ impl CompactionConfigPartial {
|
||||
compact_worker_max_input_tokens: upper
|
||||
.compact_worker_max_input_tokens
|
||||
.or(self.compact_worker_max_input_tokens),
|
||||
model: merge_option(self.model, upper.model, ModelConfigPartial::merge),
|
||||
model: merge_option(self.model, upper.model, ModelManifest::merge),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -303,29 +280,6 @@ fn ensure_absolute(field: &'static str, path: &Path) -> Result<(), ResolveError>
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_model(
|
||||
cfg: ModelConfigPartial,
|
||||
scheme_field: &'static str,
|
||||
model_id_field: &'static str,
|
||||
auth_file_field: &'static str,
|
||||
) -> Result<ModelConfig, ResolveError> {
|
||||
let scheme = cfg.scheme.ok_or(ResolveError::MissingField(scheme_field))?;
|
||||
let model_id = cfg
|
||||
.model_id
|
||||
.ok_or(ResolveError::MissingField(model_id_field))?;
|
||||
let auth = cfg.auth.unwrap_or_default();
|
||||
if let AuthRef::ApiKey { file: Some(p), .. } = &auth {
|
||||
ensure_absolute(auth_file_field, p)?;
|
||||
}
|
||||
Ok(ModelConfig {
|
||||
scheme,
|
||||
base_url: cfg.base_url,
|
||||
model_id,
|
||||
auth,
|
||||
capability: cfg.capability,
|
||||
})
|
||||
}
|
||||
|
||||
/// `AuthRef::ApiKey { file, .. }` が相対パスのとき `base` を前置する。
|
||||
fn resolve_auth_file(auth: &mut Option<AuthRef>, base: &Path) {
|
||||
if let Some(AuthRef::ApiKey { file: Some(p), .. }) = auth.as_mut() {
|
||||
@@ -333,6 +287,16 @@ fn resolve_auth_file(auth: &mut Option<AuthRef>, base: &Path) {
|
||||
}
|
||||
}
|
||||
|
||||
/// モデル宣言に含まれる `auth.file` が絶対パスであることを検証する。
|
||||
/// ref / scheme / model_id 等の論理的な有効性(ref があるか、inline が
|
||||
/// 揃っているか)の検証はカタログを知る `crates/provider` 側で行う。
|
||||
fn validate_model_paths(model: &ModelManifest, field: &'static str) -> Result<(), ResolveError> {
|
||||
if let Some(AuthRef::ApiKey { file: Some(p), .. }) = &model.auth {
|
||||
ensure_absolute(field, p)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl TryFrom<PodManifestConfig> for PodManifest {
|
||||
type Error = ResolveError;
|
||||
|
||||
@@ -346,12 +310,7 @@ impl TryFrom<PodManifestConfig> for PodManifest {
|
||||
ensure_absolute("pod.prompt_pack", p)?;
|
||||
}
|
||||
|
||||
let model = resolve_model(
|
||||
cfg.model,
|
||||
"model.scheme",
|
||||
"model.model_id",
|
||||
"model.auth.file",
|
||||
)?;
|
||||
validate_model_paths(&cfg.model, "model.auth.file")?;
|
||||
|
||||
let worker = WorkerManifest {
|
||||
instruction: cfg
|
||||
@@ -384,17 +343,9 @@ impl TryFrom<PodManifestConfig> for PodManifest {
|
||||
let compaction = cfg
|
||||
.compaction
|
||||
.map(|c| -> Result<CompactionConfig, ResolveError> {
|
||||
let comp_model = c
|
||||
.model
|
||||
.map(|p| {
|
||||
resolve_model(
|
||||
p,
|
||||
"compaction.model.scheme",
|
||||
"compaction.model.model_id",
|
||||
"compaction.model.auth.file",
|
||||
)
|
||||
})
|
||||
.transpose()?;
|
||||
if let Some(ref cm) = c.model {
|
||||
validate_model_paths(cm, "compaction.model.auth.file")?;
|
||||
}
|
||||
Ok(CompactionConfig {
|
||||
prune_protected_turns: c
|
||||
.prune_protected_turns
|
||||
@@ -413,14 +364,14 @@ impl TryFrom<PodManifestConfig> for PodManifest {
|
||||
compact_worker_max_input_tokens: c
|
||||
.compact_worker_max_input_tokens
|
||||
.unwrap_or(defaults::COMPACT_WORKER_MAX_INPUT_TOKENS),
|
||||
model: comp_model,
|
||||
model: c.model,
|
||||
})
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
Ok(PodManifest {
|
||||
pod: PodMeta { name, prompt_pack },
|
||||
model,
|
||||
model: cfg.model,
|
||||
worker,
|
||||
scope: cfg.scope,
|
||||
compaction,
|
||||
@@ -431,6 +382,7 @@ impl TryFrom<PodManifestConfig> for PodManifest {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::model::SchemeKind;
|
||||
use crate::{Permission, ScopeRule};
|
||||
|
||||
fn abs(path: &str) -> PathBuf {
|
||||
@@ -450,7 +402,7 @@ mod tests {
|
||||
name: Some("test".into()),
|
||||
prompt_pack: None,
|
||||
},
|
||||
model: ModelConfigPartial {
|
||||
model: ModelManifest {
|
||||
scheme: Some(SchemeKind::Anthropic),
|
||||
model_id: Some("claude-sonnet-4-20250514".into()),
|
||||
..Default::default()
|
||||
@@ -472,7 +424,7 @@ mod tests {
|
||||
fn resolve_minimal_succeeds() {
|
||||
let manifest: PodManifest = minimal_valid().try_into().unwrap();
|
||||
assert_eq!(manifest.pod.name, "test");
|
||||
assert_eq!(manifest.model.scheme, SchemeKind::Anthropic);
|
||||
assert_eq!(manifest.model.scheme, Some(SchemeKind::Anthropic));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -570,7 +522,7 @@ mod tests {
|
||||
name: Some("lower".into()),
|
||||
prompt_pack: None,
|
||||
},
|
||||
model: ModelConfigPartial {
|
||||
model: ModelManifest {
|
||||
model_id: Some("lower-model".into()),
|
||||
..Default::default()
|
||||
},
|
||||
@@ -743,7 +695,7 @@ permission = "write"
|
||||
name: Some("x".into()),
|
||||
prompt_pack: None,
|
||||
},
|
||||
model: ModelConfigPartial {
|
||||
model: ModelManifest {
|
||||
scheme: Some(SchemeKind::Anthropic),
|
||||
model_id: Some("m".into()),
|
||||
..Default::default()
|
||||
@@ -796,7 +748,32 @@ name = "dbg"
|
||||
let merged = builtin.merge(user).merge(project).merge(overlay);
|
||||
let manifest: PodManifest = merged.try_into().unwrap();
|
||||
assert_eq!(manifest.pod.name, "dbg");
|
||||
assert_eq!(manifest.model.scheme, SchemeKind::Anthropic);
|
||||
assert_eq!(manifest.model.scheme, Some(SchemeKind::Anthropic));
|
||||
assert_eq!(manifest.scope.allow.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_preserves_ref() {
|
||||
let lower = PodManifestConfig {
|
||||
model: ModelManifest {
|
||||
ref_: Some("anthropic/claude-sonnet-4-6".into()),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let upper = PodManifestConfig {
|
||||
model: ModelManifest {
|
||||
// only override auth
|
||||
auth: Some(AuthRef::None),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
let merged = lower.merge(upper);
|
||||
assert_eq!(
|
||||
merged.model.ref_.as_deref(),
|
||||
Some("anthropic/claude-sonnet-4-6")
|
||||
);
|
||||
assert_eq!(merged.model.auth, Some(AuthRef::None));
|
||||
}
|
||||
}
|
||||
|
||||
+14
-11
@@ -4,10 +4,10 @@ mod model;
|
||||
mod scope;
|
||||
|
||||
pub use config::{
|
||||
CompactionConfigPartial, ModelConfigPartial, PodManifestConfig, PodMetaConfig, ResolveError,
|
||||
CompactionConfigPartial, PodManifestConfig, PodMetaConfig, ResolveError,
|
||||
ToolOutputLimitsPartial, WorkerManifestConfig,
|
||||
};
|
||||
pub use model::{AuthRef, ModelConfig, SchemeKind};
|
||||
pub use model::{AuthRef, ModelCapability, ModelManifest, SchemeKind};
|
||||
pub use protocol::{Permission, ScopeRule};
|
||||
pub use scope::{Scope, ScopeError};
|
||||
|
||||
@@ -26,7 +26,7 @@ use serde::{Deserialize, Serialize};
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PodManifest {
|
||||
pub pod: PodMeta,
|
||||
pub model: ModelConfig,
|
||||
pub model: ModelManifest,
|
||||
pub worker: WorkerManifest,
|
||||
pub scope: ScopeConfig,
|
||||
#[serde(default)]
|
||||
@@ -191,7 +191,7 @@ pub struct CompactionConfig {
|
||||
/// Optional model for the compactor (summary) LLM.
|
||||
/// If omitted, the main model is cloned via `clone_boxed()`.
|
||||
#[serde(default)]
|
||||
pub model: Option<ModelConfig>,
|
||||
pub model: Option<ModelManifest>,
|
||||
}
|
||||
|
||||
fn default_prune_protected_turns() -> usize {
|
||||
@@ -255,9 +255,12 @@ permission = "write"
|
||||
fn parse_minimal_manifest() {
|
||||
let manifest = PodManifest::from_toml(MINIMAL_REQUIRED).unwrap();
|
||||
assert_eq!(manifest.pod.name, "test-agent");
|
||||
assert_eq!(manifest.model.scheme, SchemeKind::Anthropic);
|
||||
assert_eq!(manifest.model.model_id, "claude-sonnet-4-20250514");
|
||||
assert_eq!(manifest.model.auth, AuthRef::None);
|
||||
assert_eq!(manifest.model.scheme, Some(SchemeKind::Anthropic));
|
||||
assert_eq!(
|
||||
manifest.model.model_id.as_deref(),
|
||||
Some("claude-sonnet-4-20250514")
|
||||
);
|
||||
assert!(manifest.model.auth.is_none());
|
||||
assert_eq!(manifest.scope.allow.len(), 1);
|
||||
assert!(manifest.scope.deny.is_empty());
|
||||
assert_eq!(manifest.worker.instruction, defaults::DEFAULT_INSTRUCTION);
|
||||
@@ -294,8 +297,8 @@ permission = "write"
|
||||
"#;
|
||||
let manifest = PodManifest::from_toml(toml).unwrap();
|
||||
assert_eq!(manifest.pod.name, "code-reviewer");
|
||||
let file = match &manifest.model.auth {
|
||||
AuthRef::ApiKey { file, .. } => file.as_deref(),
|
||||
let file = match manifest.model.auth.as_ref() {
|
||||
Some(AuthRef::ApiKey { file, .. }) => file.as_deref(),
|
||||
_ => panic!("expected ApiKey"),
|
||||
};
|
||||
assert_eq!(file, Some(std::path::Path::new("/abs/keys/anthropic")));
|
||||
@@ -398,8 +401,8 @@ model_id = "claude-sonnet-4-20250514"
|
||||
let manifest = PodManifest::from_toml(&toml).unwrap();
|
||||
let c = manifest.compaction.unwrap();
|
||||
let p = c.model.unwrap();
|
||||
assert_eq!(p.scheme, SchemeKind::Gemini);
|
||||
assert_eq!(p.model_id, "gemini-2.0-flash");
|
||||
assert_eq!(p.scheme, Some(SchemeKind::Gemini));
|
||||
assert_eq!(p.model_id.as_deref(), Some("gemini-2.0-flash"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
//! LLM モデル宣言型
|
||||
//!
|
||||
//! Pod マニフェストの `[model]` セクションで記述する型。`scheme` と
|
||||
//! `auth` を直交軸として表現し、1 つの汎用アダプタ(`crates/provider`)
|
||||
//! で任意の wire / 認証組合せを受け止める。
|
||||
//! Pod マニフェストの `[model]` セクションで記述する型。`ref`(プロバイダ
|
||||
//! とモデルを両方指し示す短縮形)と inline 指定(`scheme` / `model_id`
|
||||
//! 直書き)の両方を受け入れるため、すべてのフィールドを `Option` として
|
||||
//! 持つ 1 つの型 [`ModelManifest`] に統合している。実解決(ref をプロバイダ
|
||||
//! カタログ / モデルカタログから引いて `scheme` や `model_id` を埋める)
|
||||
//! は `crates/provider` の責務で、本モジュールはデータ表現のみを提供する。
|
||||
//!
|
||||
//! 同じ型を partial(カスケード層)と完成形(最終マニフェスト)の両方で
|
||||
//! 使うことで、merge と最終変換の重複を避ける。
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -12,27 +18,57 @@ use serde::{Deserialize, Serialize};
|
||||
// マニフェストで任意に override できるよう型だけ再エクスポートする。
|
||||
pub use llm_worker::llm_client::capability::ModelCapability;
|
||||
|
||||
/// Pod が使う LLM モデルの宣言。
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ModelConfig {
|
||||
/// wire format
|
||||
pub scheme: SchemeKind,
|
||||
/// API のベース URL。未指定なら scheme の既定値にフォールバック
|
||||
#[serde(default)]
|
||||
/// Pod マニフェストの `[model]` セクション。
|
||||
///
|
||||
/// - ref だけ書く: `[model] ref = "anthropic/claude-sonnet-4-6"`
|
||||
/// - ref + 一部 override: ref で基底を引き、`auth` 等だけ書き換え
|
||||
/// - 完全 inline: `ref` を省略して `scheme` / `model_id` / `auth` を直書き
|
||||
///
|
||||
/// どの形が有効かの判定は `provider::resolve_model_manifest` が担う。
|
||||
/// 本クレートは「どこから取るか」を表現するだけで、未設定かどうかを
|
||||
/// 理由にした hard error は出さない。
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
|
||||
pub struct ModelManifest {
|
||||
/// `<provider_id>/<model_id_in_ref>` 形式のカタログ参照。`/` の
|
||||
/// 最初の 1 文字目で split し provider カタログを引く。
|
||||
/// OpenRouter の `anthropic/claude-sonnet-4` のように `/` を含む
|
||||
/// model_id は `openrouter/anthropic/claude-sonnet-4` と書く
|
||||
/// (provider 側で最初の `/` のみ split するため)。
|
||||
#[serde(default, rename = "ref", skip_serializing_if = "Option::is_none")]
|
||||
pub ref_: Option<String>,
|
||||
/// wire format の明示指定。ref 未指定時は必須、ref 指定時は override。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub scheme: Option<SchemeKind>,
|
||||
/// API のベース URL。scheme の既定値を override する。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub base_url: Option<String>,
|
||||
/// プロバイダが受け付けるモデル ID
|
||||
pub model_id: String,
|
||||
/// 認証方式
|
||||
#[serde(default)]
|
||||
pub auth: AuthRef,
|
||||
/// モデル能力の明示指定。`None` のときは `crates/provider` が
|
||||
/// scheme 静的テーブル → scheme 既定値の順でフォールバックする。
|
||||
/// OpenAI 互換ルーター(OpenRouter / xAI / Groq 等)で scheme テーブル
|
||||
/// に載っていないモデル ID を使うときに指定する。
|
||||
#[serde(default)]
|
||||
/// プロバイダが受け付けるモデル ID。ref 未指定時は必須。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub model_id: Option<String>,
|
||||
/// 認証方式。ref 未指定時は必須、ref 指定時は override。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub auth: Option<AuthRef>,
|
||||
/// モデル能力の明示指定。未指定時はモデルカタログ → provider
|
||||
/// `default_capability` → scheme 既定の順で解決される。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub capability: Option<ModelCapability>,
|
||||
}
|
||||
|
||||
impl ModelManifest {
|
||||
/// `upper` を `self` に上書きマージする。マニフェスト cascade 向け
|
||||
/// (builtin → user → project → overlay の優先順位で呼ばれる)。
|
||||
pub fn merge(self, upper: Self) -> Self {
|
||||
Self {
|
||||
ref_: upper.ref_.or(self.ref_),
|
||||
scheme: upper.scheme.or(self.scheme),
|
||||
base_url: upper.base_url.or(self.base_url),
|
||||
model_id: upper.model_id.or(self.model_id),
|
||||
auth: upper.auth.or(self.auth),
|
||||
capability: upper.capability.or(self.capability),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// サポートする wire scheme の種類。
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
|
||||
Reference in New Issue
Block a user