mod render;
use std::io::Write;
use serde::Serialize;
use crate::auth::{fetch_cred, AuthCtx, CredSource};
use crate::canonical::{CanonicalError, ErrorKind};
use crate::config::provider::{AuthId, Provider};
use crate::config::{config_path, defaults, partial_from_env, read_config_file, OutMode};
use crate::registry::Registry;
use crate::store::{CredStore, Secret};
pub struct ProvidersIo<'a> {
pub stdout: &'a mut dyn Write,
pub stderr: &'a mut dyn Write,
pub store: &'a dyn CredStore,
}
#[derive(Serialize)]
pub(super) struct Row {
pub(super) name: String,
pub(super) protocol: String,
pub(super) auth: String,
pub(super) effort: bool,
pub(super) priority: bool,
pub(super) tools: bool,
pub(super) multi_turn: bool,
pub(super) device: Option<String>,
pub(super) credential: &'static str,
}
pub fn list_providers(args: &crate::cli::Args, io: &mut ProvidersIo) -> u8 {
match run_list(args, io) {
Ok(code) => code,
Err(e) => {
let _ = writeln!(io.stderr, "{}", e.message);
e.exit_code()
}
}
}
fn run_list(args: &crate::cli::Args, io: &mut ProvidersIo) -> Result<u8, CanonicalError> {
let flags = crate::cli::parse_args(&args.argv)?;
if flags.help {
return Ok(super::emit(io.stdout, super::HELP));
}
if flags.skill {
return Ok(super::emit(io.stdout, super::SKILL));
}
if flags.version {
return Ok(super::emit(io.stdout, super::VERSION_LINE));
}
let file = read_config_file(&config_path(flags.config_path, &args.env))?;
let env = partial_from_env(&args.env).map_err(CanonicalError::from)?;
let merged = flags.config.or(env).or(file).or(defaults());
let json = merged.output == Some(OutMode::Ndjson);
let inline = merged.api_key.clone();
let rows: Vec<Row> = merged
.into_rows()?
.iter()
.map(|p| row(p, inline.as_ref(), io.store))
.collect();
render::print_rows(io.stdout, &rows, json).map_err(write_failed)?;
Ok(0)
}
fn row(provider: &Provider, inline: Option<&Secret>, store: &dyn CredStore) -> Row {
let dialect = Registry::builtin().protocol(provider.protocol);
let tuning = dialect.tuning();
let shapes = dialect.shapes();
Row {
name: provider.name.clone(),
protocol: spelling(&provider.protocol),
auth: spelling(&provider.auth),
effort: takes(provider, tuning.effort, "reasoning"),
priority: takes(provider, tuning.priority, "service_tier"),
tools: shapes.tools,
multi_turn: shapes.multi_turn,
device: provider
.oauth
.as_ref()
.and_then(|o| o.device.as_ref())
.map(|d| spelling(&d.style)),
credential: credential(provider, inline, store),
}
}
fn takes(provider: &Provider, projected: bool, canonical_key: &str) -> bool {
projected
&& !provider
.unsupported_body_keys
.iter()
.any(|k| k == canonical_key)
}
fn spelling<T: Serialize>(id: &T) -> String {
serde_json::to_value(id)
.ok()
.and_then(|v| v.as_str().map(str::to_owned))
.unwrap_or_default()
}
fn credential(provider: &Provider, inline: Option<&Secret>, store: &dyn CredStore) -> &'static str {
if provider.auth == AuthId::None {
return "not required";
}
if provider.auth != AuthId::OAuth2 && inline.is_some() {
return "inline";
}
let ctx = AuthCtx {
store_key: &provider.name,
inline_key: None,
api_header: provider.api_header.as_ref(),
oauth: provider.oauth.as_ref(),
ambient: provider.ambient.as_ref(),
};
match fetch_cred(store, &ctx) {
Some(f) if matches!(f.source, CredSource::Owned) => "stored",
Some(_) => "ambient",
None => "missing",
}
}
fn write_failed(e: std::io::Error) -> CanonicalError {
CanonicalError {
kind: ErrorKind::Transport,
message: format!("failed to write provider list: {e}"),
provider_detail: None,
retry_after_seconds: None,
}
}