llm-model-config完了

This commit is contained in:
2026-04-20 00:57:27 +09:00
parent 230936274b
commit 5aea67ff5e
19 changed files with 147 additions and 270 deletions
@@ -24,3 +24,17 @@ pub(crate) fn lookup(model_id: &str) -> Option<ModelCapability> {
})
}
/// Scheme 既定の capability。
///
/// Ollama の `/v1/messages` 流用を想定して `cache_control` を送らない
/// `CacheStrategy::Auto` にする。Anthropic 本家の未知モデル(新 Claude)
/// も tool_calling / vision を備える想定で Parallel / true を返す。
pub(crate) fn default_capability() -> ModelCapability {
ModelCapability {
tool_calling: ToolCallingSupport::Parallel,
structured_output: StructuredOutput::JsonSchema,
reasoning: None,
vision: false,
prompt_caching: CacheStrategy::Auto,
}
}
@@ -96,4 +96,8 @@ impl Scheme for AnthropicScheme {
fn capability_for(&self, model_id: &str) -> Option<ModelCapability> {
super::capability::lookup(model_id)
}
fn default_capability(&self) -> ModelCapability {
super::capability::default_capability()
}
}
@@ -4,6 +4,17 @@ use crate::llm_client::capability::{
CacheStrategy, ModelCapability, ReasoningSupport, StructuredOutput, ToolCallingSupport,
};
/// Scheme 既定の capability(未知モデル / 未明示モデル用)。
pub(crate) fn default_capability() -> ModelCapability {
ModelCapability {
tool_calling: ToolCallingSupport::Parallel,
structured_output: StructuredOutput::JsonSchema,
reasoning: None,
vision: true,
prompt_caching: CacheStrategy::Auto,
}
}
pub(crate) fn lookup(model_id: &str) -> Option<ModelCapability> {
if !model_id.starts_with("gemini-") {
return None;
@@ -50,4 +50,8 @@ impl Scheme for GeminiScheme {
fn capability_for(&self, model_id: &str) -> Option<ModelCapability> {
super::capability::lookup(model_id)
}
fn default_capability(&self) -> ModelCapability {
super::capability::default_capability()
}
}
+17 -4
View File
@@ -15,9 +15,10 @@ use serde_json::Value;
use super::auth::AuthRequirement;
use super::capability::ModelCapability;
use super::client::ConfigWarning;
use super::error::ClientError;
use super::event::Event;
use super::types::Request;
use super::types::{Request, RequestConfig};
/// wire scheme の抽象。各プロバイダの API 仕様ごとに 1 つ実装する。
///
@@ -44,7 +45,7 @@ pub trait Scheme: Clone + Send + Sync + 'static {
fn path(&self, model_id: &str) -> String;
/// この scheme が要求する認証形式。`build_client` 時に
/// [`AuthRef`](../../../manifest/enum.AuthRef.html) と照合する。
/// `manifest::AuthRef` と照合する。
fn required_auth(&self) -> AuthRequirement;
/// `Content-Type` 以外の追加ヘッダ。`anthropic-version` / `anthropic-beta` 等。
@@ -75,8 +76,20 @@ pub trait Scheme: Clone + Send + Sync + 'static {
) -> Result<Vec<Event>, ClientError>;
/// 既知モデル ID の能力テーブル引き。未知なら `None` を返す
/// ので、呼び出し側は scheme ごとの安全側デフォルト
/// [`ModelCapability::minimal`])にフォールバックする。
/// ので、呼び出し側は [`Scheme::default_capability`] に
/// フォールバックする。
fn capability_for(&self, model_id: &str) -> Option<ModelCapability>;
/// scheme 既定の capability。未知モデル ID や未明示モデルでの
/// フォールバックに使う。`capability_for` と違って必ず値を返す。
fn default_capability(&self) -> ModelCapability;
/// scheme 側でサポートしていない `RequestConfig` フィールドを
/// 警告として返す(例: OpenAI Chat は `top_k` 非対応)。
/// デフォルトは空 Vec。
fn validate_config(&self, config: &RequestConfig) -> Vec<ConfigWarning> {
let _ = config;
Vec::new()
}
}
@@ -45,3 +45,15 @@ pub(crate) fn lookup(model_id: &str) -> Option<ModelCapability> {
}
None
}
/// Scheme 既定の capability。OpenAI 互換ルーター系(xAI / Groq / OpenRouter 等)
/// で未知モデル ID を受けたときのフォールバックに使う。
pub(crate) fn default_capability() -> ModelCapability {
ModelCapability {
tool_calling: ToolCallingSupport::Parallel,
structured_output: StructuredOutput::JsonSchema,
reasoning: None,
vision: false,
prompt_caching: CacheStrategy::Auto,
}
}
@@ -4,11 +4,12 @@ use serde_json::Value;
use crate::llm_client::{
ClientError,
capability::ModelCapability,
event::Event,
auth::AuthRequirement,
capability::ModelCapability,
client::ConfigWarning,
event::Event,
scheme::Scheme,
types::Request,
types::{Request, RequestConfig},
};
use super::OpenAIScheme;
@@ -54,4 +55,17 @@ impl Scheme for OpenAIScheme {
fn capability_for(&self, model_id: &str) -> Option<ModelCapability> {
super::capability::lookup(model_id)
}
fn default_capability(&self) -> ModelCapability {
super::capability::default_capability()
}
fn validate_config(&self, config: &RequestConfig) -> Vec<ConfigWarning> {
let mut warnings = Vec::new();
// OpenAI Chat Completions API は top_k を受け付けない
if config.top_k.is_some() {
warnings.push(ConfigWarning::unsupported("top_k", "OpenAI Chat"));
}
warnings
}
}
@@ -11,13 +11,13 @@ use eventsource_stream::Eventsource;
use futures::{Stream, StreamExt, TryStreamExt};
use reqwest::header::{CONTENT_TYPE, HeaderMap, HeaderValue};
use super::auth::AuthRequirement;
use super::capability::ModelCapability;
use super::client::LlmClient;
use super::client::{ConfigWarning, LlmClient};
use super::error::ClientError;
use super::event::Event;
use super::auth::AuthRequirement;
use super::scheme::Scheme;
use super::types::Request;
use super::types::{Request, RequestConfig};
/// `AuthRef` を解決したランタイム表現。`crates/provider` が構築する。
///
@@ -155,6 +155,10 @@ impl<S: Scheme + Clone + 'static> LlmClient for HttpTransport<S> {
Box::new(self.clone())
}
fn validate_config(&self, config: &RequestConfig) -> Vec<ConfigWarning> {
self.scheme.validate_config(config)
}
async fn stream(
&self,
request: Request,
+1
View File
@@ -5,6 +5,7 @@ edition.workspace = true
license.workspace = true
[dependencies]
llm-worker = { version = "0.2.1", path = "../llm-worker" }
protocol = { version = "0.1.0", path = "../protocol" }
serde = { version = "1.0.228", features = ["derive"] }
serde_ignored = "0.1.14"
+4
View File
@@ -50,6 +50,8 @@ pub struct ModelConfigPartial {
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)]
@@ -205,6 +207,7 @@ impl ModelConfigPartial {
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),
}
}
}
@@ -310,6 +313,7 @@ fn resolve_model(
base_url: cfg.base_url,
model_id,
auth,
capability: cfg.capability,
})
}
+10
View File
@@ -8,6 +8,10 @@ use std::path::PathBuf;
use serde::{Deserialize, Serialize};
// `ModelCapability` は `llm-worker` 側に定義される runtime 構造だが、
// マニフェストで任意に override できるよう型だけ再エクスポートする。
pub use llm_worker::llm_client::capability::ModelCapability;
/// Pod が使う LLM モデルの宣言。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ModelConfig {
@@ -21,6 +25,12 @@ pub struct ModelConfig {
/// 認証方式
#[serde(default)]
pub auth: AuthRef,
/// モデル能力の明示指定。`None` のときは `crates/provider` が
/// scheme 静的テーブル → scheme 既定値の順でフォールバックする。
/// OpenAI 互換ルーター(OpenRouter / xAI / Groq 等)で scheme テーブル
/// に載っていないモデル ID を使うときに指定する。
#[serde(default)]
pub capability: Option<ModelCapability>,
}
/// サポートする wire scheme の種類。
+2
View File
@@ -361,6 +361,7 @@ fn build_overlay_toml(
base_url: model.base_url.clone(),
model_id: Some(model.model_id.clone()),
auth: Some(model.auth.clone()),
capability: model.capability.clone(),
},
worker: WorkerManifestConfig {
instruction: Some(instruction.to_string()),
@@ -494,6 +495,7 @@ mod tests {
env: None,
file: Some(PathBuf::from("/etc/keys/anthropic")),
},
capability: None,
};
let toml_str = build_overlay_toml("child", "$insomnia/default", &[], &model).unwrap();
+1
View File
@@ -141,6 +141,7 @@ fn dummy_model() -> ModelConfig {
base_url: None,
model_id: "claude-test".into(),
auth: AuthRef::None,
capability: None,
}
}
+1
View File
@@ -12,3 +12,4 @@ thiserror = "2.0"
[dev-dependencies]
serial_test = "3.4.0"
tempfile = "3.27.0"
toml = "1.1.2"
+40 -32
View File
@@ -12,7 +12,7 @@
use llm_worker::llm_client::{
LlmClient,
capability::{CacheStrategy, ModelCapability, StructuredOutput, ToolCallingSupport},
capability::ModelCapability,
scheme::{
Scheme, anthropic::AnthropicScheme, gemini::GeminiScheme, openai_chat::OpenAIScheme,
},
@@ -79,34 +79,6 @@ fn resolve_auth(
}
}
/// `SchemeKind` ごとに固定のデフォルト capability(未知モデル用)。
fn default_capability(scheme: SchemeKind) -> ModelCapability {
match scheme {
SchemeKind::Anthropic => ModelCapability {
tool_calling: ToolCallingSupport::Parallel,
structured_output: StructuredOutput::JsonSchema,
reasoning: None,
vision: false,
// Ollama の /v1/messages 流用時に cache_control を拒否されないよう Auto
prompt_caching: CacheStrategy::Auto,
},
SchemeKind::OpenaiChat | SchemeKind::OpenaiResponses => ModelCapability {
tool_calling: ToolCallingSupport::Parallel,
structured_output: StructuredOutput::JsonSchema,
reasoning: None,
vision: false,
prompt_caching: CacheStrategy::Auto,
},
SchemeKind::Gemini => ModelCapability {
tool_calling: ToolCallingSupport::Parallel,
structured_output: StructuredOutput::JsonSchema,
reasoning: None,
vision: true,
prompt_caching: CacheStrategy::Auto,
},
}
}
fn build_transport<S: Scheme>(
scheme: S,
config: &ModelConfig,
@@ -117,9 +89,16 @@ fn build_transport<S: Scheme>(
scheme: config.scheme,
});
}
let capability = scheme
.capability_for(&config.model_id)
.unwrap_or_else(|| default_capability(config.scheme));
// capability の優先順位:
// 1. `ModelConfig.capability` の明示指定(OpenAI 互換ルーターの
// 未知モデル等、マニフェストで完全に上書きしたいケース)
// 2. scheme 静的テーブル(既知モデル)
// 3. `Scheme::default_capability()`scheme ごとの安全側デフォルト)
let capability: ModelCapability = config
.capability
.clone()
.or_else(|| scheme.capability_for(&config.model_id))
.unwrap_or_else(|| scheme.default_capability());
let base_url = config
.base_url
.clone()
@@ -162,6 +141,7 @@ mod tests {
env: None,
file: None,
},
capability: None,
}
}
@@ -247,6 +227,33 @@ mod tests {
assert!(matches!(result, Err(ProviderError::ApiKeyMissing { .. })));
}
#[test]
fn model_config_capability_overrides_scheme_default() {
// 未知モデル ID でも `ModelConfig.capability` が指定されていれば
// scheme の静的テーブル / デフォルトではなくその値が採用される。
use llm_worker::llm_client::capability::{
CacheStrategy, ModelCapability, ReasoningEffort, ReasoningSupport, StructuredOutput,
ToolCallingSupport,
};
let explicit = ModelCapability {
tool_calling: ToolCallingSupport::Parallel,
structured_output: StructuredOutput::JsonSchema,
reasoning: Some(ReasoningSupport::Effort),
vision: true,
prompt_caching: CacheStrategy::Auto,
};
// TOML 経由の往復(`[model.capability]` が正しくパースできる)
let toml_str = toml::to_string(&explicit).unwrap();
let round_trip: ModelCapability = toml::from_str(&toml_str).unwrap();
assert_eq!(round_trip, explicit);
// `_ = ReasoningEffort` は serde derive が欠けていると失敗する
// ほぼ確実なコンパイル時ガード。
let _ = ReasoningEffort::Medium;
}
#[test]
fn ollama_succeeds_without_key() {
// Ollama = Anthropic scheme + base_url 差し替え + AuthRef::None
@@ -255,6 +262,7 @@ mod tests {
base_url: Some("http://localhost:11434".into()),
model_id: "llama3".into(),
auth: AuthRef::None,
capability: None,
};
// scheme.required_auth() が XApiKey でも ResolvedAuth::None は許容する
// None は全 scheme で受け入れるため)