llm-model-config完了
This commit is contained in:
parent
230936274b
commit
5aea67ff5e
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -1517,6 +1517,7 @@ dependencies = [
|
|||
name = "manifest"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"llm-worker",
|
||||
"protocol",
|
||||
"serde",
|
||||
"serde_ignored",
|
||||
|
|
@ -1983,6 +1984,7 @@ dependencies = [
|
|||
"serial_test",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
1
TODO.md
1
TODO.md
|
|
@ -3,7 +3,6 @@
|
|||
- [ ] Bash ツール (Permission 層と統合) → [tickets/bash-tool.md](tickets/bash-tool.md)
|
||||
- [ ] Protocol の設計 → [tickets/protocol-design.md](tickets/protocol-design.md)
|
||||
- [ ] LLM プロバイダ統合
|
||||
- [ ] モデル設定の構造再編(providers 層廃止 + Pod 宣言化) → [tickets/llm-model-config.md](tickets/llm-model-config.md)
|
||||
- [ ] OpenAI Responses scheme の新設 → [tickets/llm-scheme-openai-responses.md](tickets/llm-scheme-openai-responses.md)
|
||||
- [ ] Codex OAuth 認証の流用 → [tickets/llm-auth-codex-oauth.md](tickets/llm-auth-codex-oauth.md)
|
||||
- [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 の種類。
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ fn dummy_model() -> ModelConfig {
|
|||
base_url: None,
|
||||
model_id: "claude-test".into(),
|
||||
auth: AuthRef::None,
|
||||
capability: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -12,3 +12,4 @@ thiserror = "2.0"
|
|||
[dev-dependencies]
|
||||
serial_test = "3.4.0"
|
||||
tempfile = "3.27.0"
|
||||
toml = "1.1.2"
|
||||
|
|
|
|||
|
|
@ -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 で受け入れるため)
|
||||
|
|
|
|||
|
|
@ -1,122 +0,0 @@
|
|||
# LLM モデル設定の再編
|
||||
|
||||
> **レビュー中** — 詳細は [`llm-model-config.review.md`](llm-model-config.review.md)
|
||||
> 主な指摘: 要件 6 の `ModelConfig.capability` override 未実装、`validate_config` の機能退化(OpenAI top_k warning 消失)。どちらも判断待ち。
|
||||
|
||||
|
||||
## 背景
|
||||
|
||||
決定済みの LLM プロバイダサポート方針(`docs/plan/llm_providers.md`)に従って llm-worker のプロバイダ層を再編する。Pod 側で「使う LLM モデル」を宣言する構造にし、共通の通信層 + scheme の組合せで任意のプロバイダを収容できるようにする。
|
||||
|
||||
### 現状の問題
|
||||
|
||||
- `crates/llm-worker/src/llm_client/providers/{anthropic,openai,gemini,ollama}.rs` は各々 HTTP 骨格 + scheme 呼び出しの薄いアダプタで、構造が重複
|
||||
- `providers/ollama.rs` (67行) は scheme 未分離で整合性が崩れている
|
||||
- OpenAI 互換ルーター系(xAI / Groq / OpenRouter / Together / BLACKBOX 等)を追加するたびに新ファイルを書く必要がある
|
||||
|
||||
## 要件
|
||||
|
||||
1. **Pod マニフェストで LLM モデルを宣言できる**
|
||||
|
||||
```
|
||||
ModelConfig {
|
||||
scheme: Scheme, // Anthropic / OpenAIChat / OpenAIResponses / Gemini
|
||||
base_url: Url,
|
||||
model_id: String,
|
||||
auth: AuthRef,
|
||||
}
|
||||
```
|
||||
|
||||
継承(commit ebee0b9)と整合し、親 Pod の定義を子 Pod が override 可能。
|
||||
|
||||
2. **`providers/` 層の廃止**: llm-worker は `HttpTransport<S: Scheme>` 相当の汎用通信層 1 本を持ち、`ModelConfig` を食わせてインスタンス化する
|
||||
|
||||
3. **既存 scheme の再編**:
|
||||
- `scheme/openai` を `scheme/openai_chat` にリネーム
|
||||
- `scheme/anthropic` / `scheme/gemini` はそのまま
|
||||
- `scheme/openai_responses` は別チケット(llm-scheme-openai-responses)で新設
|
||||
- Ollama は **scheme/anthropic を base_url 差し替えで流用**(独自 scheme は作らない)
|
||||
|
||||
4. **認証の分離**: `AuthRef` は `ApiKey(EnvVar | ConfigRef)` / `CodexOAuth` / `None` を表現でき、scheme とは直交する層で管理される。`CodexOAuth` の実装自体は別チケット(llm-auth-codex-oauth)
|
||||
|
||||
5. **決定済みプロバイダ方針との整合**:
|
||||
- 第一級: Ollama / Codex OAuth / Anthropic API
|
||||
- 二次: OpenAI 互換共通枠(`{ scheme: OpenAIChat, base_url: 各社, auth: ApiKey }` の宣言だけで収容)
|
||||
|
||||
6. **ModelCapability を分離**: モデルに紐づく機能差を別メタデータとして表現
|
||||
|
||||
```
|
||||
ModelCapability {
|
||||
tool_calling: ToolCallingSupport, // parallel 可否含む
|
||||
structured_output: StructuredOutput, // JsonObject / JsonSchema
|
||||
reasoning: Option<ReasoningSupport>, // effort / budget_tokens
|
||||
vision: bool,
|
||||
prompt_caching: CacheStrategy, // Explicit { max_breakpoints } / Auto
|
||||
}
|
||||
```
|
||||
|
||||
`ReasoningControl { effort, budget_tokens }` は共通型、scheme アダプタで各社形式に投影。プロバイダ側高次ツール(web_search 等)は採用しないため軸から除外。
|
||||
|
||||
7. **Streaming は現状維持**: 既存 `BlockStart / BlockDelta / BlockStop / BlockAbort` + `DeltaContent::{Text, Thinking, InputJson}` を変更しない。Gemini や Ollama のように ToolCall 引数 delta を送らないプロバイダは scheme アダプタで「BlockStart → InputJson(全体 1 回) → BlockStop」の擬似ストリーム化で吸収
|
||||
|
||||
8. **Ollama 運用の注意点**: scheme/anthropic 流用前提で以下を守る
|
||||
- `cache_control` は送らない(`ModelCapability::prompt_caching = Auto`)
|
||||
- `tool_choice` / `metadata` / URL 画像は送らない(Ollama 側非対応)
|
||||
- `/v1/messages/count_tokens` は叩かない(issue #13949 でサーバ不安定化)
|
||||
- `/v1/chat/completions` は stream+tools バグ (#9092) のため使わない
|
||||
|
||||
9. **完了時の動作**: 既存の動作は変わらず、Pod マニフェストで `ModelConfig` を宣言するだけでモデル切替できる。OpenAI 互換の新規プロバイダは新コードなしで追加可能
|
||||
|
||||
## 設計決定
|
||||
|
||||
### 1. `Scheme` trait の境界(方針A: 全面抽象化)
|
||||
|
||||
trait で URL 組立・認証要件・body 変換・SSE パースをすべて抽象化し、`HttpTransport` は 1 本にする。trait スケッチ:
|
||||
|
||||
```rust
|
||||
trait Scheme {
|
||||
fn path(&self, model: &str) -> String; // "/v1/messages" 等
|
||||
fn required_auth(&self) -> AuthRequirement; // Bearer / XApiKey / QueryParam / None
|
||||
fn additional_headers(&self) -> Vec<(&str, String)>; // anthropic-version 等
|
||||
fn build_request_body(&self, model: &str, req: &Request, cap: &ModelCapability) -> Value;
|
||||
fn parse_sse(&self, event_type: &str, data: &str) -> Result<Vec<Event>, ClientError>;
|
||||
fn default_base_url(&self) -> &'static str;
|
||||
}
|
||||
```
|
||||
|
||||
`parse_sse` は `Vec<Event>` に統一(Anthropic は 1 要素 Vec で扱う)。
|
||||
|
||||
### 2. `AuthRef` と `Scheme` の組合せ検証(方針B: 構築時検証)
|
||||
|
||||
`Scheme::required_auth()` で要求する `AuthRequirement` を宣言し、`build_client` 時に `AuthRef` と照合。非対応組合せは構築エラーにする(実行時に落とさない)。
|
||||
|
||||
### 3. `crates/provider/` の去就(方針A: 残す)
|
||||
|
||||
`provider` クレートは保持。`build_client(ModelConfig) -> Box<dyn LlmClient>` は `(Scheme, AuthRef)` 照合 + `HttpTransport::new` の薄ラッパーに縮退する。`~/.codex/auth.json` 読み取り等の認証ストア解決は `provider` クレートに肉付けしていく(llm-worker は低レベル基盤に留める方針と整合)。
|
||||
|
||||
### 4. `ModelCapability` の保持(方針: ハイブリッド)
|
||||
|
||||
- scheme 実装側に `model_id → ModelCapability` の静的テーブルを持つ(既知モデル分)
|
||||
- `ModelConfig` で明示宣言すれば override
|
||||
- 未知モデルは scheme ごとの安全側デフォルト(`prompt_caching: Auto` 等)にフォールバック
|
||||
|
||||
今チケットのスコープ:
|
||||
- 型定義 + 既知モデルの固定値
|
||||
- `CacheStrategy::Explicit` で `cache_control` マーカー挿入(既存実装を capability で分岐)
|
||||
- `ReasoningControl { effort, budget_tokens }` を scheme 側で各社形式に投影
|
||||
- `CacheStrategy::Auto` は何もしない(Ollama で重要)
|
||||
|
||||
### 5. マニフェスト継承との統合(方針: フィールド単位 override)
|
||||
|
||||
`ProviderConfigPartial` と同じ方針で、`ModelConfig` の全フィールド(`scheme` / `base_url` / `model_id` / `auth`)を `Option<T>` で継承。子 Pod は `base_url` だけ差し替える等が可能。
|
||||
|
||||
### 6. TOML 後方互換
|
||||
|
||||
旧 `[provider] kind = "..."` フォーマットは互換を切る。新 `[model]` セクションで `scheme` / `base_url` / `model_id` / `auth` を宣言する。
|
||||
|
||||
## Scope 外
|
||||
|
||||
- OpenAI Responses scheme の新設(`tickets/llm-scheme-openai-responses.md`)
|
||||
- Codex OAuth 認証アダプタの実装(`tickets/llm-auth-codex-oauth.md`)
|
||||
- OpenAI 互換ルーター各社の動作確認
|
||||
- プロバイダ選択 UI(将来の native GUI / TUI 拡張)
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
# LLM モデル設定の再編 — レビュー
|
||||
|
||||
## 前提・要件の再確認
|
||||
|
||||
`docs/plan/llm_providers.md` の第一級/二次/非サポート方針と、チケット本体の 9 要件 + 6 設計決定 を前提に、実装が意図と整合しているかを確認した。変更量は +1608 / -1174 行、41 ファイル。
|
||||
|
||||
`cargo check` および `cargo test --workspace --lib` は通過。
|
||||
|
||||
## 要件達成度
|
||||
|
||||
| # | 要件 | 状況 | メモ |
|
||||
|---|---|---|---|
|
||||
| 1 | Pod マニフェスト `[model]` 宣言 | ✓ | `manifest/src/model.rs` + `config.rs::ModelConfigPartial` |
|
||||
| 2 | `providers/` 層廃止 | ✓ | 4 ファイル削除、`HttpTransport<S>` 1 本に集約 |
|
||||
| 3 | 既存 scheme 再編(openai → openai_chat、Ollama は流用) | ✓ | 各 scheme に `scheme_impl.rs` + `capability.rs` 追加 |
|
||||
| 4 | `AuthRef` 分離 | ✓ | `manifest/model.rs` + `llm_client/auth.rs::AuthRequirement` |
|
||||
| 5 | 第一級/二次 方針整合 | ✓ | `provider/lib.rs::build_client` + `SchemeKind` |
|
||||
| 6 | `ModelCapability` 分離 | △ | 型定義・静的テーブル・default はあり。**`ModelConfig` からのマニフェスト override は未実装** |
|
||||
| 7 | Streaming 現状維持 | ✓ | Event 型変更なし、`Scheme::State` で Anthropic block_type 補完 |
|
||||
| 8 | Ollama 運用注意点 | ✓ | `default_capability` で Anthropic scheme が `CacheStrategy::Auto`、`ollama_succeeds_without_key` テストあり |
|
||||
| 9 | 完了時動作 | ✓ | ビルド通過、既存テスト通過 |
|
||||
|
||||
## 設計決定の反映
|
||||
|
||||
| # | 決定 | 反映 |
|
||||
|---|---|---|
|
||||
| 1 | `Scheme` trait 方針A(全面抽象化) | ✓ `scheme/mod.rs::Scheme` に URL/認証/ヘッダ/body/SSE を集約。`State` associated type で Anthropic の `block_type` 補完を綺麗に処理 |
|
||||
| 2 | `AuthRef` 組合せ検証 方針B(構築時) | ✓ `ResolvedAuth::matches` + `build_transport` で照合 |
|
||||
| 3 | `crates/provider` 方針A(残す) | ✓ 薄いファクトリとして維持 |
|
||||
| 4 | `ModelCapability` ハイブリッド | △ scheme 側静的テーブルはあるが、マニフェスト override 側が欠落 |
|
||||
| 5 | フィールド単位 override | ✓ `ModelConfigPartial::merge` |
|
||||
| 6 | TOML 後方互換切り | ✓ 旧 `[provider]` は完全に新 `[model]` に置換 |
|
||||
|
||||
## 指摘事項
|
||||
|
||||
### 優先度: 中
|
||||
|
||||
#### 1. 要件 6 の「ModelConfig で明示宣言すれば override」が未実装
|
||||
|
||||
`ModelConfig` 構造体に `capability: Option<ModelCapability>` フィールドがない。設計決定 4(ハイブリッド: scheme 側テーブル → マニフェスト override → デフォルト)の **override 側が欠落**している。
|
||||
|
||||
影響:
|
||||
- scheme 静的テーブルに無いモデル(OpenRouter / xAI の Grok / Groq の Kimi / OpenAI 互換ルーター系)は必ず `default_capability(scheme)` に落ちる
|
||||
- 二次サポートの共通枠の実用性に直結(例: Grok の `ReasoningSupport::Effort` が効かず reasoning 送れない)
|
||||
|
||||
対応案:
|
||||
- A. 今チケットで `ModelConfig.capability: Option<ModelCapability>` を追加し `build_transport` で優先順位 `config.capability → scheme.capability_for → default_capability` に
|
||||
- B. Scope 外として明示し別チケットに切り出す
|
||||
|
||||
ユーザーの決定方針(二次サポートを共通枠でカバー)からすると A が自然。ticket 本体の Scope 外にも記載がないため、A を推奨。
|
||||
|
||||
#### 2. `validate_config` の機能退化
|
||||
|
||||
旧 `providers/openai.rs` の `OpenAIClient` は `LlmClient::validate_config` をオーバーライドし「OpenAI は `top_k` 非対応」の warning を出していた。削除された `tests/validation_test.rs` はこの warning をテストしていた。
|
||||
|
||||
今の `HttpTransport<S: Scheme>` は `validate_config` をオーバーライドしておらず、`LlmClient` trait のデフォルト実装(空 `Vec`)が使われる。つまり **`Worker::validate()` は scheme による制約(top_k, logprobs 等)を検出できなくなっている**。
|
||||
|
||||
対応案:
|
||||
- `Scheme` trait に `validate_config(&RequestConfig) -> Vec<ConfigWarning>` を追加し、`HttpTransport` 側で scheme に委譲
|
||||
- 最低限 OpenAI Chat scheme で旧と同じ top_k warning を再実装
|
||||
- 合わせて新形式での regression test を追加(旧 `validation_test.rs` の代替)
|
||||
|
||||
### 優先度: 低
|
||||
|
||||
#### 3. `default_capability` の配置
|
||||
|
||||
`crates/provider/lib.rs::default_capability` が `SchemeKind` ごとに直書き(同じ情報が `Scheme` trait 実装側と分離して存在)。`Scheme::default_capability()` メソッドに移動する方が関心事が集約される。今の形でも動作するが、新 scheme 追加時に 2 箇所編集が必要な点が弱い。
|
||||
|
||||
#### 4. `AuthRequirement` 判定の緩さ
|
||||
|
||||
`ResolvedAuth::matches` は `(None, _) => true` で常にパス。これは Ollama Anthropic 流用(`AuthRef::None` で `XApiKey` 要求)のための意図的設計だが、本来認証必須の scheme(Anthropic 本家)に誤って `AuthRef::None` を渡しても構築成功し、実行時の 401 で初めて失敗する。
|
||||
|
||||
より厳密にするなら `AuthRequirement::XApiKeyOptional` のようなバリアント導入で分離できるが、実害は小さいので現状維持も許容範囲。
|
||||
|
||||
#### 5. rustdoc のクロス crate リンク
|
||||
|
||||
`scheme/mod.rs:47` の `[AuthRef](../../../manifest/enum.AuthRef.html)` は相対リンクで、`cargo doc --workspace` 時に切れる可能性。`[`manifest::AuthRef`]` 形式のクロス crate リンクにしておくと rustdoc が解決できる。
|
||||
|
||||
## アーキテクチャ評価
|
||||
|
||||
### 良い点
|
||||
- `Scheme::State` associated type の導入で「Anthropic の `content_block_stop` に `block_type` が載らない」といった具体的な痛みを抽象内で解決
|
||||
- `ResolvedAuth::matches` による構築時検証が `build_transport` 1 箇所に集約、分岐が明瞭
|
||||
- `ModelConfigPartial::merge` のフィールド単位 override が既存の cascade layer と自然に噛み合う
|
||||
- Ollama 運用の制約(`cache_control`/`tool_choice`/`metadata` 不可)が capability + scheme 側送出制御で分散、`provider::tests::ollama_succeeds_without_key` で境界条件がテストされている
|
||||
- `spawn_pod.rs::overlay_inherits_spawner_model` テストで親 Pod の `ModelConfig` が子にシームレスに伝播することを確認
|
||||
|
||||
### コードベースを歪めていないか
|
||||
- 旧 `providers/` の 4 ファイルは綺麗に削除、重複は残っていない
|
||||
- `SchemeKind::OpenaiResponses` はマニフェスト側に先行存在するが、`build_client` で `SchemeNotImplemented` エラーを明示的に返す(別チケットで肉付け前提)。これは依存チケット設計通り
|
||||
- `AuthRef::CodexOAuth` も同様に予約のみ、`resolve_auth` でエラーを返す
|
||||
|
||||
### 不必要な実装
|
||||
- `AuthRequirement::Custom` バリアントは Codex OAuth 用の先行予約で、今チケットでは使われない。将来の拡張点として小さい負債、許容範囲
|
||||
|
||||
## 総合判定
|
||||
|
||||
**コア要件は達成されている。構造再編は綺麗で、Scope の切り方も妥当**。実装ミス的な重大欠陥はなく、既存テストも全て通過している。
|
||||
|
||||
ただし以下 2 点の判断が必要:
|
||||
|
||||
1. **要件 6 の `ModelConfig.capability` override**: 今チケットで追加するか、Scope 外として明示するか
|
||||
2. **`validate_config` の退化**: 復活させるか、ticket 本体に「OpenAI の top_k warning 等の validate 機能は scheme 再編で意図的に落とした」旨を明記するか
|
||||
|
||||
どちらも「今 close して後で別チケット」でも進められるが、最低限ticket 本体への記載(Scope 外明示 or 後続タスク言及)が必要。
|
||||
Loading…
Reference in New Issue
Block a user