128 lines
6.4 KiB
Rust
128 lines
6.4 KiB
Rust
//! LLM モデル宣言型
|
||
//!
|
||
//! Pod マニフェストの `[model]` セクションで記述する型。`ref`(プロバイダ
|
||
//! とモデルを両方指し示す短縮形)と inline 指定(`scheme` / `model_id`
|
||
//! 直書き)の両方を受け入れるため、すべてのフィールドを `Option` として
|
||
//! 持つ 1 つの型 [`ModelManifest`] に統合している。実解決(ref をプロバイダ
|
||
//! カタログ / モデルカタログから引いて `scheme` や `model_id` を埋める)
|
||
//! は `crates/provider` の責務で、本モジュールはデータ表現のみを提供する。
|
||
//!
|
||
//! 同じ型を partial(カスケード層)と完成形(最終マニフェスト)の両方で
|
||
//! 使うことで、merge と最終変換の重複を避ける。
|
||
|
||
use std::path::PathBuf;
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
|
||
// `ModelCapability` は `llm-worker` 側に定義される runtime 構造だが、
|
||
// マニフェストで任意に override できるよう型だけ再エクスポートする。
|
||
pub use llm_worker::llm_client::capability::{ModelCapability, ReasoningControl, ReasoningEffort};
|
||
|
||
/// 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。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>,
|
||
/// モデルの希望コンテキストウィンドウ(tokens)。カタログ未掲載 / inline
|
||
/// モデルでもここで明示 override できる。実効値は `max_context_window`
|
||
/// またはカタログ上の backend maximum で clamp される。
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub context_window: Option<u64>,
|
||
/// backend が実際に受け付けるコンテキストウィンドウ上限(tokens)。
|
||
/// 表示・安全判定に使う実効 context window は `context_window` とこの値の
|
||
/// 小さい方になる。
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub max_context_window: Option<u64>,
|
||
}
|
||
|
||
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),
|
||
context_window: upper.context_window.or(self.context_window),
|
||
max_context_window: upper.max_context_window.or(self.max_context_window),
|
||
}
|
||
}
|
||
}
|
||
|
||
/// サポートする wire scheme の種類。
|
||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||
#[serde(rename_all = "snake_case")]
|
||
pub enum SchemeKind {
|
||
/// Anthropic Messages API (`/v1/messages`)。Ollama `/v1/messages` もこれで扱う
|
||
Anthropic,
|
||
/// OpenAI Chat Completions (`/v1/chat/completions`)。OpenAI 互換ルーター共通枠
|
||
OpenaiChat,
|
||
/// OpenAI Responses API (`/v1/responses`)。別チケットで scheme 新設予定
|
||
OpenaiResponses,
|
||
/// Google Gemini (`/v1beta/models/...:streamGenerateContent`)
|
||
Gemini,
|
||
}
|
||
|
||
/// 認証の参照。
|
||
///
|
||
/// 実際のトークン値の解決(local secret store / file 読取、OAuth refresh 等)は
|
||
/// `crates/provider` で行う。ここはあくまで「どこから取るか」の宣言。
|
||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||
pub enum AuthRef {
|
||
/// 認証不要(ローカル Ollama 等)
|
||
#[default]
|
||
None,
|
||
/// API key file reference. Prefer [`AuthRef::SecretRef`] for normal
|
||
/// provider credentials; this remains an explicit file source for low-level
|
||
/// manifests and tests.
|
||
ApiKey {
|
||
/// key を書き込んだファイル(絶対パス)
|
||
#[serde(default)]
|
||
file: Option<PathBuf>,
|
||
},
|
||
/// ChatGPT OAuth(`~/.codex/auth.json`)。実装は `llm-auth-codex-oauth` チケット
|
||
#[serde(rename = "codex_oauth")]
|
||
CodexOAuth,
|
||
/// Typed local secret-store reference. The profile resolver preserves this
|
||
/// reference verbatim; secret-store lookup/deobfuscation is intentionally a
|
||
/// later consumer-boundary concern.
|
||
#[serde(rename = "secret_ref")]
|
||
SecretRef {
|
||
#[serde(rename = "ref")]
|
||
ref_: String,
|
||
},
|
||
}
|