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
+4 -3
View File
@@ -1,13 +1,14 @@
# manifest
Pod の宣言的設定を TOML マニフェストとして定義・パースするクレート。プロバイダ設定、ワーカー設定、ディレクトリスコープ制約を記述できる。
Pod の宣言的設定を TOML マニフェストとして定義・パースするクレート。モデル設定、ワーカー設定、ディレクトリスコープ制約を記述できる。
## 公開型
- `PodManifest` — Pod 設定全体(`from_toml()` でパース)
- `PodMeta` — Pod メタデータ(名前、pwd)
- `ProviderConfig` — LLM プロバイダ設定(種別、モデル、APIキー環境変数、ベースURL
- `ProviderKind`プロバイダ種別(`Anthropic`, `Openai`, `Gemini`, `Ollama`
- `ModelConfig` — LLM モデル設定(scheme、base_url、model_id、auth
- `SchemeKind`wire scheme 種別(`Anthropic`, `OpenaiChat`, `OpenaiResponses`, `Gemini`
- `AuthRef` — 認証参照(`None`, `ApiKey { env, file }`, `CodexOAuth`
- `WorkerManifest` — ワーカー設定(システムプロンプト、max_tokens、temperature
- `ScopeConfig` / `ScopeRule` / `Permission` — allow / deny の宣言的スコープ設定
- `Scope` — 実行時スコープ。`from_config(&ScopeConfig, pwd)` で構築し、`is_readable` / `is_writable` / `permission_at` で問い合わせる
+97 -81
View File
@@ -13,10 +13,8 @@ use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::defaults;
use crate::{
CompactionConfig, PodManifest, PodMeta, ProviderConfig, ProviderKind, ScopeConfig,
ToolOutputLimits, WorkerManifest,
};
use crate::model::{AuthRef, ModelConfig, SchemeKind};
use crate::{CompactionConfig, PodManifest, PodMeta, ScopeConfig, ToolOutputLimits, WorkerManifest};
/// Partial-form Pod manifest. Every field is optional; one or more
/// instances merge via [`PodManifestConfig::merge`] before being
@@ -26,7 +24,7 @@ pub struct PodManifestConfig {
#[serde(default)]
pub pod: PodMetaConfig,
#[serde(default)]
pub provider: ProviderConfigPartial,
pub model: ModelConfigPartial,
#[serde(default)]
pub worker: WorkerManifestConfig,
#[serde(default)]
@@ -41,16 +39,17 @@ pub struct PodMetaConfig {
pub name: Option<String>,
}
/// Partial-form of [`ModelConfig`]. カスケード層で個別に与えられる。
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ProviderConfigPartial {
pub struct ModelConfigPartial {
#[serde(default)]
pub kind: Option<ProviderKind>,
#[serde(default)]
pub model: Option<String>,
#[serde(default)]
pub api_key_file: Option<PathBuf>,
pub scheme: Option<SchemeKind>,
#[serde(default)]
pub base_url: Option<String>,
#[serde(default)]
pub model_id: Option<String>,
#[serde(default)]
pub auth: Option<AuthRef>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
@@ -92,7 +91,7 @@ pub struct CompactionConfigPartial {
#[serde(default)]
pub compact_worker_max_input_tokens: Option<u64>,
#[serde(default)]
pub provider: Option<ProviderConfigPartial>,
pub model: Option<ModelConfigPartial>,
}
/// Errors raised when converting a [`PodManifestConfig`] to a validated
@@ -148,18 +147,16 @@ impl PodManifestConfig {
/// rules from different layers do not accidentally inherit another
/// layer's base.
///
/// Affected fields: `provider.api_key_file`,
/// Affected fields: `model.auth.file`,
/// `scope.allow[].target`, `scope.deny[].target`,
/// `compaction.provider.api_key_file`.
/// `compaction.model.auth.file`.
pub fn resolve_paths(mut self, base: &Path) -> Self {
debug_assert!(
base.is_absolute(),
"resolve_paths base must be absolute: {}",
base.display()
);
if let Some(ref mut p) = self.provider.api_key_file {
*p = join_if_relative(base, p);
}
resolve_auth_file(&mut self.model.auth, base);
for rule in &mut self.scope.allow {
rule.target = join_if_relative(base, &rule.target);
}
@@ -167,10 +164,9 @@ impl PodManifestConfig {
rule.target = join_if_relative(base, &rule.target);
}
if let Some(ref mut compaction) = self.compaction
&& let Some(ref mut cp) = compaction.provider
&& let Some(ref mut p) = cp.api_key_file
&& let Some(ref mut cp) = compaction.model
{
*p = join_if_relative(base, p);
resolve_auth_file(&mut cp.auth, base);
}
self
}
@@ -182,7 +178,7 @@ impl PodManifestConfig {
pub fn merge(self, upper: PodManifestConfig) -> Self {
Self {
pod: self.pod.merge(upper.pod),
provider: self.provider.merge(upper.provider),
model: self.model.merge(upper.model),
worker: self.worker.merge(upper.worker),
scope: merge_scope(self.scope, upper.scope),
compaction: merge_option(
@@ -202,13 +198,13 @@ impl PodMetaConfig {
}
}
impl ProviderConfigPartial {
impl ModelConfigPartial {
fn merge(self, upper: Self) -> Self {
Self {
kind: upper.kind.or(self.kind),
model: upper.model.or(self.model),
api_key_file: upper.api_key_file.or(self.api_key_file),
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),
}
}
}
@@ -254,7 +250,7 @@ impl CompactionConfigPartial {
compact_worker_max_input_tokens: upper
.compact_worker_max_input_tokens
.or(self.compact_worker_max_input_tokens),
provider: merge_option(self.provider, upper.provider, ProviderConfigPartial::merge),
model: merge_option(self.model, upper.model, ModelConfigPartial::merge),
}
}
}
@@ -295,25 +291,35 @@ fn ensure_absolute(field: &'static str, path: &Path) -> Result<(), ResolveError>
}
}
fn resolve_provider(
cfg: ProviderConfigPartial,
kind_field: &'static str,
model_field: &'static str,
api_key_field: &'static str,
) -> Result<ProviderConfig, ResolveError> {
let kind = cfg.kind.ok_or(ResolveError::MissingField(kind_field))?;
let model = cfg.model.ok_or(ResolveError::MissingField(model_field))?;
if let Some(ref p) = cfg.api_key_file {
ensure_absolute(api_key_field, p)?;
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(ProviderConfig {
kind,
model,
api_key_file: cfg.api_key_file,
Ok(ModelConfig {
scheme,
base_url: cfg.base_url,
model_id,
auth,
})
}
/// `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() {
*p = join_if_relative(base, p);
}
}
impl TryFrom<PodManifestConfig> for PodManifest {
type Error = ResolveError;
@@ -323,11 +329,11 @@ impl TryFrom<PodManifestConfig> for PodManifest {
.name
.ok_or(ResolveError::MissingField("pod.name"))?;
let provider = resolve_provider(
cfg.provider,
"provider.kind",
"provider.model",
"provider.api_key_file",
let model = resolve_model(
cfg.model,
"model.scheme",
"model.model_id",
"model.auth.file",
)?;
let worker = WorkerManifest {
@@ -361,14 +367,14 @@ impl TryFrom<PodManifestConfig> for PodManifest {
let compaction = cfg
.compaction
.map(|c| -> Result<CompactionConfig, ResolveError> {
let comp_provider = c
.provider
let comp_model = c
.model
.map(|p| {
resolve_provider(
resolve_model(
p,
"compaction.provider.kind",
"compaction.provider.model",
"compaction.provider.api_key_file",
"compaction.model.scheme",
"compaction.model.model_id",
"compaction.model.auth.file",
)
})
.transpose()?;
@@ -390,14 +396,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),
provider: comp_provider,
model: comp_model,
})
})
.transpose()?;
Ok(PodManifest {
pod: PodMeta { name },
provider,
model,
worker,
scope: cfg.scope,
compaction,
@@ -414,14 +420,21 @@ mod tests {
PathBuf::from(format!("/tmp/insomnia-test{path}"))
}
fn api_key_file_auth(path: PathBuf) -> AuthRef {
AuthRef::ApiKey {
env: None,
file: Some(path),
}
}
fn minimal_valid() -> PodManifestConfig {
PodManifestConfig {
pod: PodMetaConfig {
name: Some("test".into()),
},
provider: ProviderConfigPartial {
kind: Some(ProviderKind::Anthropic),
model: Some("claude-sonnet-4-20250514".into()),
model: ModelConfigPartial {
scheme: Some(SchemeKind::Anthropic),
model_id: Some("claude-sonnet-4-20250514".into()),
..Default::default()
},
worker: WorkerManifestConfig::default(),
@@ -441,16 +454,20 @@ mod tests {
fn resolve_minimal_succeeds() {
let manifest: PodManifest = minimal_valid().try_into().unwrap();
assert_eq!(manifest.pod.name, "test");
assert_eq!(manifest.provider.kind, ProviderKind::Anthropic);
assert_eq!(manifest.model.scheme, SchemeKind::Anthropic);
}
#[test]
fn resolve_paths_joins_relative_api_key_file() {
fn resolve_paths_joins_relative_auth_file() {
let mut cfg = minimal_valid();
cfg.provider.api_key_file = Some(PathBuf::from("keys/anthropic"));
cfg.model.auth = Some(api_key_file_auth(PathBuf::from("keys/anthropic")));
let resolved = cfg.resolve_paths(Path::new("/home/user/.config/insomnia"));
let file = match resolved.model.auth {
Some(AuthRef::ApiKey { file, .. }) => file,
_ => panic!("expected ApiKey"),
};
assert_eq!(
resolved.provider.api_key_file.as_deref(),
file.as_deref(),
Some(Path::new("/home/user/.config/insomnia/keys/anthropic"))
);
}
@@ -458,12 +475,13 @@ mod tests {
#[test]
fn resolve_paths_leaves_absolute_paths_untouched() {
let mut cfg = minimal_valid();
cfg.provider.api_key_file = Some(PathBuf::from("/etc/already/abs"));
cfg.model.auth = Some(api_key_file_auth(PathBuf::from("/etc/already/abs")));
let resolved = cfg.resolve_paths(Path::new("/home/user"));
assert_eq!(
resolved.provider.api_key_file.as_deref(),
Some(Path::new("/etc/already/abs"))
);
let file = match resolved.model.auth {
Some(AuthRef::ApiKey { file, .. }) => file,
_ => panic!("expected ApiKey"),
};
assert_eq!(file.as_deref(), Some(Path::new("/etc/already/abs")));
}
#[test]
@@ -484,16 +502,14 @@ mod tests {
}
#[test]
fn try_from_invariant_rejects_lingering_relative_api_key_file() {
fn try_from_invariant_rejects_lingering_relative_auth_file() {
let mut cfg = minimal_valid();
cfg.provider.api_key_file = Some(PathBuf::from("keys/relative"));
// Skipping resolve_paths on purpose: TryFrom must catch the
// invariant violation.
cfg.model.auth = Some(api_key_file_auth(PathBuf::from("keys/relative")));
let err = PodManifest::try_from(cfg).unwrap_err();
assert!(matches!(
err,
ResolveError::RelativePath {
field: "provider.api_key_file",
field: "model.auth.file",
..
}
));
@@ -535,8 +551,8 @@ mod tests {
pod: PodMetaConfig {
name: Some("lower".into()),
},
provider: ProviderConfigPartial {
model: Some("lower-model".into()),
model: ModelConfigPartial {
model_id: Some("lower-model".into()),
..Default::default()
},
..Default::default()
@@ -549,8 +565,8 @@ mod tests {
};
let merged = lower.merge(upper);
assert_eq!(merged.pod.name.as_deref(), Some("upper"));
// model not present in upper — retain lower
assert_eq!(merged.provider.model.as_deref(), Some("lower-model"));
// model_id not present in upper — retain lower
assert_eq!(merged.model.model_id.as_deref(), Some("lower-model"));
}
#[test]
@@ -706,9 +722,9 @@ permission = "write"
pod: PodMetaConfig {
name: Some("x".into()),
},
provider: ProviderConfigPartial {
kind: Some(ProviderKind::Anthropic),
model: Some("m".into()),
model: ModelConfigPartial {
scheme: Some(SchemeKind::Anthropic),
model_id: Some("m".into()),
..Default::default()
},
scope: ScopeConfig {
@@ -734,9 +750,9 @@ permission = "write"
let builtin = PodManifestConfig::default();
let user = PodManifestConfig::from_toml(
r#"
[provider]
kind = "anthropic"
model = "claude-sonnet-4-20250514"
[model]
scheme = "anthropic"
model_id = "claude-sonnet-4-20250514"
"#,
)
.unwrap();
@@ -759,7 +775,7 @@ 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.provider.kind, ProviderKind::Anthropic);
assert_eq!(manifest.model.scheme, SchemeKind::Anthropic);
assert_eq!(manifest.scope.allow.len(), 1);
}
}
+40 -75
View File
@@ -1,30 +1,31 @@
mod config;
pub mod defaults;
mod model;
mod scope;
pub use config::{
CompactionConfigPartial, PodManifestConfig, PodMetaConfig, ProviderConfigPartial, ResolveError,
CompactionConfigPartial, ModelConfigPartial, PodManifestConfig, PodMetaConfig, ResolveError,
ToolOutputLimitsPartial, WorkerManifestConfig,
};
pub use model::{AuthRef, ModelConfig, SchemeKind};
pub use protocol::{Permission, ScopeRule};
pub use scope::{Scope, ScopeError};
use std::collections::HashMap;
use std::num::NonZeroU32;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
/// Declarative configuration for a Pod.
///
/// Parsed from a TOML manifest file. Describes the provider, model,
/// system prompt, and directory scope (required). The Pod's working
/// directory is **not** part of the manifest — it is the process's
/// `std::env::current_dir()` at construction time.
/// Parsed from a TOML manifest file. Describes the model, system prompt,
/// and directory scope (required). The Pod's working directory is **not**
/// part of the manifest — it is the process's `std::env::current_dir()`
/// at construction time.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PodManifest {
pub pod: PodMeta,
pub provider: ProviderConfig,
pub model: ModelConfig,
pub worker: WorkerManifest,
pub scope: ScopeConfig,
#[serde(default)]
@@ -37,44 +38,6 @@ pub struct PodMeta {
pub name: String,
}
/// LLM provider configuration.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderConfig {
pub kind: ProviderKind,
pub model: String,
/// Path to a file containing the API key (read and trimmed at startup).
#[serde(default)]
pub api_key_file: Option<PathBuf>,
/// Custom base URL for the provider API.
#[serde(default)]
pub base_url: Option<String>,
}
/// Supported LLM providers.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ProviderKind {
Anthropic,
Openai,
Gemini,
Ollama,
}
impl ProviderKind {
/// Conventional environment variable name for the API key.
///
/// Returns `INSOMNIA_API_KEY_{KIND}` (e.g. `INSOMNIA_API_KEY_ANTHROPIC`).
pub fn env_var_name(self) -> String {
let kind = match self {
Self::Anthropic => "ANTHROPIC",
Self::Openai => "OPENAI",
Self::Gemini => "GEMINI",
Self::Ollama => "OLLAMA",
};
format!("INSOMNIA_API_KEY_{kind}")
}
}
/// Worker-level configuration embedded in the manifest.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorkerManifest {
@@ -211,10 +174,10 @@ pub struct CompactionConfig {
#[serde(default = "default_compact_worker_max_input_tokens")]
pub compact_worker_max_input_tokens: u64,
/// Optional provider for the compactor (summary) LLM.
/// If omitted, the main provider is cloned via `clone_boxed()`.
/// Optional model for the compactor (summary) LLM.
/// If omitted, the main model is cloned via `clone_boxed()`.
#[serde(default)]
pub provider: Option<ProviderConfig>,
pub model: Option<ModelConfig>,
}
fn default_prune_protected_turns() -> usize {
@@ -243,7 +206,7 @@ impl Default for CompactionConfig {
compact_retained_tokens: default_compact_retained_tokens(),
compact_auto_read_budget: default_compact_auto_read_budget(),
compact_worker_max_input_tokens: default_compact_worker_max_input_tokens(),
provider: None,
model: None,
}
}
}
@@ -263,9 +226,9 @@ mod tests {
[pod]
name = "test-agent"
[provider]
kind = "anthropic"
model = "claude-sonnet-4-20250514"
[model]
scheme = "anthropic"
model_id = "claude-sonnet-4-20250514"
[worker]
@@ -278,9 +241,9 @@ permission = "write"
fn parse_minimal_manifest() {
let manifest = PodManifest::from_toml(MINIMAL_REQUIRED).unwrap();
assert_eq!(manifest.pod.name, "test-agent");
assert_eq!(manifest.provider.kind, ProviderKind::Anthropic);
assert_eq!(manifest.provider.model, "claude-sonnet-4-20250514");
assert!(manifest.provider.api_key_file.is_none());
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.scope.allow.len(), 1);
assert!(manifest.scope.deny.is_empty());
assert_eq!(manifest.worker.instruction, defaults::DEFAULT_INSTRUCTION);
@@ -292,10 +255,10 @@ permission = "write"
[pod]
name = "code-reviewer"
[provider]
kind = "anthropic"
model = "claude-sonnet-4-20250514"
api_key_file = "/abs/keys/anthropic"
[model]
scheme = "anthropic"
model_id = "claude-sonnet-4-20250514"
auth = { kind = "api_key", file = "/abs/keys/anthropic" }
[worker]
instruction = "$user/reviewer"
@@ -317,10 +280,11 @@ permission = "write"
"#;
let manifest = PodManifest::from_toml(toml).unwrap();
assert_eq!(manifest.pod.name, "code-reviewer");
assert_eq!(
manifest.provider.api_key_file.as_deref(),
Some(std::path::Path::new("/abs/keys/anthropic"))
);
let file = match &manifest.model.auth {
AuthRef::ApiKey { file, .. } => file.as_deref(),
_ => panic!("expected ApiKey"),
};
assert_eq!(file, Some(std::path::Path::new("/abs/keys/anthropic")));
assert_eq!(manifest.worker.instruction, "$user/reviewer");
assert_eq!(manifest.worker.max_tokens, Some(4096));
assert_eq!(manifest.worker.temperature, Some(0.3));
@@ -340,9 +304,9 @@ permission = "write"
[pod]
name = "missing-scope"
[provider]
kind = "anthropic"
model = "claude-sonnet-4-20250514"
[model]
scheme = "anthropic"
model_id = "claude-sonnet-4-20250514"
[worker]
"#;
@@ -408,20 +372,20 @@ model = "claude-sonnet-4-20250514"
}
#[test]
fn parse_compaction_with_provider() {
fn parse_compaction_with_model() {
let toml = format!(
"{MINIMAL_REQUIRED}\n\
[compaction]\n\
compact_threshold = 80000\n\n\
[compaction.provider]\n\
kind = \"gemini\"\n\
model = \"gemini-2.0-flash\"\n"
[compaction.model]\n\
scheme = \"gemini\"\n\
model_id = \"gemini-2.0-flash\"\n"
);
let manifest = PodManifest::from_toml(&toml).unwrap();
let c = manifest.compaction.unwrap();
let p = c.provider.unwrap();
assert_eq!(p.kind, ProviderKind::Gemini);
assert_eq!(p.model, "gemini-2.0-flash");
let p = c.model.unwrap();
assert_eq!(p.scheme, SchemeKind::Gemini);
assert_eq!(p.model_id, "gemini-2.0-flash");
}
#[test]
@@ -431,8 +395,9 @@ model = "claude-sonnet-4-20250514"
}
#[test]
fn reject_unknown_provider() {
let toml = MINIMAL_REQUIRED.replace("kind = \"anthropic\"", "kind = \"unknown_provider\"");
fn reject_unknown_scheme() {
let toml =
MINIMAL_REQUIRED.replace("scheme = \"anthropic\"", "scheme = \"unknown_scheme\"");
assert!(PodManifest::from_toml(&toml).is_err());
}
+74
View File
@@ -0,0 +1,74 @@
//! LLM モデル宣言型
//!
//! Pod マニフェストの `[model]` セクションで記述する型。`scheme` と
//! `auth` を直交軸として表現し、1 つの汎用アダプタ(`crates/provider`
//! で任意の wire / 認証組合せを受け止める。
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
/// Pod が使う LLM モデルの宣言。
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ModelConfig {
/// wire format
pub scheme: SchemeKind,
/// API のベース URL。未指定なら scheme の既定値にフォールバック
#[serde(default)]
pub base_url: Option<String>,
/// プロバイダが受け付けるモデル ID
pub model_id: String,
/// 認証方式
#[serde(default)]
pub auth: AuthRef,
}
/// サポートする 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,
}
/// 認証の参照。
///
/// 実際のトークン値の解決(env / 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。env / file のいずれか(両方指定された場合は env が優先)
ApiKey {
/// 環境変数名。未指定のときは scheme ごとの既定(`INSOMNIA_API_KEY_*`
#[serde(default)]
env: Option<String>,
/// key を書き込んだファイル(絶対パス)
#[serde(default)]
file: Option<PathBuf>,
},
/// ChatGPT OAuth`~/.codex/auth.json`)。実装は `llm-auth-codex-oauth` チケット
CodexOAuth,
}
impl SchemeKind {
/// 既定の環境変数名(`INSOMNIA_API_KEY_*`)。
///
/// `AuthRef::ApiKey { env: None, .. }` の env 未指定時に使う。
pub fn default_env_var(self) -> &'static str {
match self {
Self::Anthropic => "INSOMNIA_API_KEY_ANTHROPIC",
Self::OpenaiChat | Self::OpenaiResponses => "INSOMNIA_API_KEY_OPENAI",
Self::Gemini => "INSOMNIA_API_KEY_GEMINI",
}
}
}