Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 15 additions & 2 deletions crates/jp_cli/src/cmd/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -911,9 +911,22 @@ fn read_api_keys() -> Result<Vec<(String, String, String)>, crate::Error> {
match env {
// Named for the chain entry that selects it.
ApiKeyEnv::One(variable) => vec![(target, "api_key".to_owned(), variable)],
ApiKeyEnv::Many(variables) => variables

// One key read from several places: a row per variable, in
// the order they are tried, so the table shows which is set.
ApiKeyEnv::FirstOf(variables) => variables
.into_iter()
.map(|variable| (target.clone(), "api_key".to_owned(), variable))
.collect(),
ApiKeyEnv::Many(keys) => keys
.into_iter()
.map(|(name, variable)| (target.clone(), name, variable))
.flat_map(|(name, variables)| {
variables
.as_slice()
.iter()
.map(|variable| (target.clone(), name.clone(), variable.clone()))
.collect::<Vec<_>>()
})
.collect(),
}
})
Expand Down
12 changes: 10 additions & 2 deletions crates/jp_config/src/providers/llm/anthropic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,11 +107,19 @@ pub struct AnthropicConfig {
/// Environment variable that contains the API key.
///
/// A map names several keys, each selectable from the `auth` chain as
/// `api_key:<name>`:
/// `api_key:<name>`.
/// Each key is a variable, or a list read the same way as below:
///
/// ```toml
/// api_key_env = { work = "WORK_ANTHROPIC_KEY", personal = "MY_ANTHROPIC_KEY" }
/// ```
///
/// A list is one key, read from the first variable that holds a non-empty
/// value:
///
/// ```toml
/// api_key_env = ["WORK_ANTHROPIC_KEY", "MY_ANTHROPIC_KEY"]
/// ```
#[setting(default = "ANTHROPIC_API_KEY")]
pub api_key_env: ApiKeyEnv,

Expand Down Expand Up @@ -157,7 +165,7 @@ impl AssignKeyValue for PartialAnthropicConfig {
fn assign(&mut self, mut kv: KvAssignment) -> AssignResult {
match kv.key_string().as_str() {
"" => kv.try_merge_object(self)?,
"api_key_env" => self.api_key_env = kv.try_some_object_or_from_str()?,
"api_key_env" => self.api_key_env = kv.try_some_value()?,
"base_url" => self.base_url = kv.try_some_string()?,
"subscription_flow" => self.subscription_flow = kv.try_some_object_or_from_str()?,
"acp_config_dirs" => {
Expand Down
12 changes: 10 additions & 2 deletions crates/jp_config/src/providers/llm/cerebras.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,19 @@ pub struct CerebrasConfig {
/// Environment variable that contains the API key.
///
/// A map names several keys, each selectable from the `auth` chain as
/// `api_key:<name>`:
/// `api_key:<name>`.
/// Each key is a variable, or a list read the same way as below:
///
/// ```toml
/// api_key_env = { work = "WORK_CEREBRAS_KEY", personal = "MY_CEREBRAS_KEY" }
/// ```
///
/// A list is one key, read from the first variable that holds a non-empty
/// value:
///
/// ```toml
/// api_key_env = ["WORK_CEREBRAS_KEY", "MY_CEREBRAS_KEY"]
/// ```
#[setting(default = "CEREBRAS_API_KEY")]
pub api_key_env: ApiKeyEnv,

Expand Down Expand Up @@ -105,7 +113,7 @@ impl AssignKeyValue for PartialCerebrasConfig {
value => Err(format!("expected a string, got {value}").into()),
})?;
}
"api_key_env" => self.api_key_env = kv.try_some_object_or_from_str()?,
"api_key_env" => self.api_key_env = kv.try_some_value()?,
"base_url" => self.base_url = kv.try_some_string()?,
_ => return missing_key(&kv),
}
Expand Down
12 changes: 10 additions & 2 deletions crates/jp_config/src/providers/llm/deepseek.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,19 @@ pub struct DeepseekConfig {
/// Environment variable that contains the API key.
///
/// A map names several keys, each selectable from the `auth` chain as
/// `api_key:<name>`:
/// `api_key:<name>`.
/// Each key is a variable, or a list read the same way as below:
///
/// ```toml
/// api_key_env = { work = "WORK_DEEPSEEK_KEY", personal = "MY_DEEPSEEK_KEY" }
/// ```
///
/// A list is one key, read from the first variable that holds a non-empty
/// value:
///
/// ```toml
/// api_key_env = ["WORK_DEEPSEEK_KEY", "MY_DEEPSEEK_KEY"]
/// ```
#[setting(default = "DEEPSEEK_API_KEY")]
pub api_key_env: ApiKeyEnv,

Expand Down Expand Up @@ -81,7 +89,7 @@ impl AssignKeyValue for PartialDeepseekConfig {
value => Err(format!("expected a string, got {value}").into()),
})?;
}
"api_key_env" => self.api_key_env = kv.try_some_object_or_from_str()?,
"api_key_env" => self.api_key_env = kv.try_some_value()?,
"base_url" => self.base_url = kv.try_some_string()?,
_ => return missing_key(&kv),
}
Expand Down
12 changes: 10 additions & 2 deletions crates/jp_config/src/providers/llm/google.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,19 @@ pub struct GoogleConfig {
/// Environment variable that contains the API key.
///
/// A map names several keys, each selectable from the `auth` chain as
/// `api_key:<name>`:
/// `api_key:<name>`.
/// Each key is a variable, or a list read the same way as below:
///
/// ```toml
/// api_key_env = { work = "WORK_GEMINI_KEY", personal = "MY_GEMINI_KEY" }
/// ```
///
/// A list is one key, read from the first variable that holds a non-empty
/// value:
///
/// ```toml
/// api_key_env = ["WORK_GEMINI_KEY", "MY_GEMINI_KEY"]
/// ```
#[setting(default = "GEMINI_API_KEY")]
pub api_key_env: ApiKeyEnv,

Expand Down Expand Up @@ -81,7 +89,7 @@ impl AssignKeyValue for PartialGoogleConfig {
value => Err(format!("expected a string, got {value}").into()),
})?;
}
"api_key_env" => self.api_key_env = kv.try_some_object_or_from_str()?,
"api_key_env" => self.api_key_env = kv.try_some_value()?,
"base_url" => self.base_url = kv.try_some_string()?,
_ => return missing_key(&kv),
}
Expand Down
12 changes: 10 additions & 2 deletions crates/jp_config/src/providers/llm/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,19 @@ pub struct OpenaiConfig {
/// Environment variable that contains the API key.
///
/// A map names several keys, each selectable from the `auth` chain as
/// `api_key:<name>`:
/// `api_key:<name>`.
/// Each key is a variable, or a list read the same way as below:
///
/// ```toml
/// api_key_env = { work = "WORK_OPENAI_KEY", personal = "MY_OPENAI_KEY" }
/// ```
///
/// A list is one key, read from the first variable that holds a non-empty
/// value:
///
/// ```toml
/// api_key_env = ["WORK_OPENAI_KEY", "MY_OPENAI_KEY"]
/// ```
#[setting(default = "OPENAI_API_KEY")]
pub api_key_env: ApiKeyEnv,

Expand Down Expand Up @@ -128,7 +136,7 @@ impl AssignKeyValue for PartialOpenaiConfig {
value => Err(format!("expected a string, got {value}").into()),
})?;
}
"api_key_env" => self.api_key_env = kv.try_some_object_or_from_str()?,
"api_key_env" => self.api_key_env = kv.try_some_value()?,
"base_url" => self.base_url = kv.try_some_string()?,
"base_url_env" => self.base_url_env = kv.try_some_string()?,
"codex_base_url" => self.codex_base_url = kv.try_some_string()?,
Expand Down
12 changes: 10 additions & 2 deletions crates/jp_config/src/providers/llm/openrouter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,19 @@ pub struct OpenrouterConfig {
/// Environment variable that contains the API key.
///
/// A map names several keys, each selectable from the `auth` chain as
/// `api_key:<name>`:
/// `api_key:<name>`.
/// Each key is a variable, or a list read the same way as below:
///
/// ```toml
/// api_key_env = { work = "WORK_OPENROUTER_KEY", personal = "MY_OPENROUTER_KEY" }
/// ```
///
/// A list is one key, read from the first variable that holds a non-empty
/// value:
///
/// ```toml
/// api_key_env = ["WORK_OPENROUTER_KEY", "MY_OPENROUTER_KEY"]
/// ```
#[setting(default = "OPENROUTER_API_KEY")]
pub api_key_env: ApiKeyEnv,

Expand Down Expand Up @@ -90,7 +98,7 @@ impl AssignKeyValue for PartialOpenrouterConfig {
value => Err(format!("expected a string, got {value}").into()),
})?;
}
"api_key_env" => self.api_key_env = kv.try_some_object_or_from_str()?,
"api_key_env" => self.api_key_env = kv.try_some_value()?,
"app_name" => self.app_name = kv.try_some_string()?,
"app_referrer" => self.app_referrer = kv.try_some_string()?,
"base_url" => self.base_url = kv.try_some_string()?,
Expand Down
12 changes: 10 additions & 2 deletions crates/jp_config/src/providers/llm/vllm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,19 @@ pub struct VllmConfig {
/// Environment variable that contains the API key.
///
/// A map names several keys, each selectable from the `auth` chain as
/// `api_key:<name>`:
/// `api_key:<name>`.
/// Each key is a variable, or a list read the same way as below:
///
/// ```toml
/// api_key_env = { work = "WORK_VLLM_KEY", personal = "MY_VLLM_KEY" }
/// ```
///
/// A list is one key, read from the first variable that holds a non-empty
/// value:
///
/// ```toml
/// api_key_env = ["WORK_VLLM_KEY", "MY_VLLM_KEY"]
/// ```
#[setting(default = "VLLM_API_KEY")]
pub api_key_env: ApiKeyEnv,

Expand Down Expand Up @@ -93,7 +101,7 @@ impl AssignKeyValue for PartialVllmConfig {
value => Err(format!("expected a string, got {value}").into()),
})?;
}
"api_key_env" => self.api_key_env = kv.try_some_object_or_from_str()?,
"api_key_env" => self.api_key_env = kv.try_some_value()?,
"base_url" => self.base_url = kv.try_some_string()?,
_ => return missing_key(&kv),
}
Expand Down
30 changes: 30 additions & 0 deletions crates/jp_config/src/providers/llm_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,36 @@ fn test_provider_config_anthropic() {
);
}

#[test]
fn api_key_env_assigns_a_list_of_variables() {
let mut p = PartialLlmProviderConfig::default();

let kv =
KvAssignment::try_from_cli("vllm.api_key_env:", r#"["WORK_KEY", "USER_KEY"]"#).unwrap();
p.assign(kv).unwrap();
assert_eq!(
p.vllm.api_key_env,
Some(ApiKeyEnv::FirstOf(vec![
"WORK_KEY".to_owned(),
"USER_KEY".to_owned()
]))
);
}

#[test]
fn api_key_env_assigns_a_map_of_variables() {
let mut p = PartialLlmProviderConfig::default();

let kv = KvAssignment::try_from_cli("vllm.api_key_env:", r#"{"work": "WORK_KEY"}"#).unwrap();
p.assign(kv).unwrap();
assert_eq!(
p.vllm.api_key_env,
Some(ApiKeyEnv::Many(
[("work".to_owned(), "WORK_KEY".into())].into()
))
);
}

#[test]
fn test_provider_config_openai() {
let mut p = PartialLlmProviderConfig::default();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1365,8 +1365,9 @@ providers: ProviderConfig
*: string
api_key_env?: ApiKeyEnv
|: string
|: [string]
|:
*: string
*: string | [string]
auth?: [string]
base_url?: string
beta_headers?: MergeableVec
Expand All @@ -1381,22 +1382,25 @@ providers: ProviderConfig
cerebras: CerebrasConfig
api_key_env?: ApiKeyEnv
|: string
|: [string]
|:
*: string
*: string | [string]
auth?: [string]
base_url?: string
deepseek: DeepseekConfig
api_key_env?: ApiKeyEnv
|: string
|: [string]
|:
*: string
*: string | [string]
auth?: [string]
base_url?: string
google: GoogleConfig
api_key_env?: ApiKeyEnv
|: string
|: [string]
|:
*: string
*: string | [string]
auth?: [string]
base_url?: string
llamacpp: LlamacppConfig
Expand All @@ -1406,8 +1410,9 @@ providers: ProviderConfig
openai: OpenaiConfig
api_key_env?: ApiKeyEnv
|: string
|: [string]
|:
*: string
*: string | [string]
auth?: [string]
base_url?: string
base_url_env?: string
Expand All @@ -1416,17 +1421,19 @@ providers: ProviderConfig
openrouter: OpenrouterConfig
api_key_env?: ApiKeyEnv
|: string
|: [string]
|:
*: string
*: string | [string]
app_name?: string
app_referrer: string | null
auth?: [string]
base_url?: string
vllm: VllmConfig
api_key_env?: ApiKeyEnv
|: string
|: [string]
|:
*: string
*: string | [string]
auth?: [string]
base_url?: string
mcp: MergeableMap_McpProviderConfig
Expand Down
Loading
Loading