Release 0.1.6
This commit is contained in:
Generated
+1
-1
@@ -232,7 +232,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cdxs"
|
name = "cdxs"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cdxs"
|
name = "cdxs"
|
||||||
version = "0.1.5"
|
version = "0.1.6"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Codex account switcher CLI"
|
description = "Codex account switcher CLI"
|
||||||
|
|
||||||
|
|||||||
@@ -42,10 +42,12 @@ Codex home 的解析顺序:
|
|||||||
- `cdxs list`:列出账号,并自动刷新过期配额缓存。
|
- `cdxs list`:列出账号,并自动刷新过期配额缓存。
|
||||||
- `cdxs show`:列出账号,但不请求配额接口。
|
- `cdxs show`:列出账号,但不请求配额接口。
|
||||||
- `cdxs switch`:不带参数时自动选择可用配额最优的账号。
|
- `cdxs switch`:不带参数时自动选择可用配额最优的账号。
|
||||||
- `cdxs switch <账号>`:切换到指定账号。
|
- `cdxs switch <账号或别名>`:切换到指定账号,可用 API 账号的 `--alias` 别名。
|
||||||
|
- `cdxs switch A --model <model> --effort <effort> --name OpenAI`:切换时更新该账号默认模型、思考程度;`--name` 仅用于 API 模式的 provider name。
|
||||||
- `cdxs ping`:以 5 并发 ping 所有 OAuth 账号,运行最低 reasoning 的最小 `codex exec`,触发 Codex 侧额度状态刷新。
|
- `cdxs ping`:以 5 并发 ping 所有 OAuth 账号,运行最低 reasoning 的最小 `codex exec`,触发 Codex 侧额度状态刷新。
|
||||||
- `cdxs ping --account <账号>`:指定账号运行最小 `codex exec`;可重复传多个账号。
|
- `cdxs ping --account <账号>`:指定账号运行最小 `codex exec`;可重复传多个账号。
|
||||||
- `cdxs login api --key <key> --base-url <url> --switch`:保存 API Key 账号并可选切换;`base-url` 为空时使用 OpenAI 默认 API。
|
- `cdxs login --api <key> --base-url <url> --alias A --model <model> --name OpenAI --switch`:保存 API Key 账号并可选切换;`base-url` 为空时使用 OpenAI 默认 API。
|
||||||
|
- `cdxs login api --key <key> --base-url <url> --alias A --model <model> --name OpenAI --switch`:同上,保留旧的子命令形式。
|
||||||
- `cdxs remove <账号>`:删除账号,等价于 `cdxs account remove <账号>`。
|
- `cdxs remove <账号>`:删除账号,等价于 `cdxs account remove <账号>`。
|
||||||
- `cdxs pull`:从同步服务拉取账号状态,等价于 `cdxs sync pull`。
|
- `cdxs pull`:从同步服务拉取账号状态,等价于 `cdxs sync pull`。
|
||||||
- `cdxs push`:推送账号状态到同步服务,等价于 `cdxs sync push`。
|
- `cdxs push`:推送账号状态到同步服务,等价于 `cdxs sync push`。
|
||||||
|
|||||||
+111
-7
@@ -16,6 +16,20 @@ use crate::{codex_config, jwt, paths, token};
|
|||||||
|
|
||||||
const DEFAULT_API_BASE_URL: &str = "https://api.openai.com/v1";
|
const DEFAULT_API_BASE_URL: &str = "https://api.openai.com/v1";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct ApiKeyOptions {
|
||||||
|
pub alias: Option<String>,
|
||||||
|
pub model: Option<String>,
|
||||||
|
pub provider_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default)]
|
||||||
|
pub struct SwitchOptions {
|
||||||
|
pub model: Option<String>,
|
||||||
|
pub reasoning_effort: Option<String>,
|
||||||
|
pub provider_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn import_auth(file: Option<PathBuf>, codex_home: Option<PathBuf>, switch: bool) -> Result<()> {
|
pub fn import_auth(file: Option<PathBuf>, codex_home: Option<PathBuf>, switch: bool) -> Result<()> {
|
||||||
// Store imported credentials in the main cdxs config, even when the source
|
// Store imported credentials in the main cdxs config, even when the source
|
||||||
// auth file came from another CODEX_HOME.
|
// auth file came from another CODEX_HOME.
|
||||||
@@ -39,10 +53,15 @@ pub fn import_auth(file: Option<PathBuf>, codex_home: Option<PathBuf>, switch: b
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_api_key(key: String, base_url: Option<String>, switch: bool) -> Result<()> {
|
pub fn add_api_key(
|
||||||
|
key: String,
|
||||||
|
base_url: Option<String>,
|
||||||
|
options: ApiKeyOptions,
|
||||||
|
switch: bool,
|
||||||
|
) -> Result<()> {
|
||||||
let home = paths::codex_home(None)?;
|
let home = paths::codex_home(None)?;
|
||||||
let mut store = Store::load(&home)?;
|
let mut store = Store::load(&home)?;
|
||||||
let account = api_key_account(key, base_url)?;
|
let account = api_key_account(key, base_url, options)?;
|
||||||
let id = account.id.clone();
|
let id = account.id.clone();
|
||||||
let email = account.email.clone();
|
let email = account.email.clone();
|
||||||
store.upsert_account(account);
|
store.upsert_account(account);
|
||||||
@@ -205,6 +224,7 @@ pub async fn switch_account(
|
|||||||
query: &str,
|
query: &str,
|
||||||
codex_home: Option<PathBuf>,
|
codex_home: Option<PathBuf>,
|
||||||
apply_fingerprint: bool,
|
apply_fingerprint: bool,
|
||||||
|
options: SwitchOptions,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
if apply_fingerprint {
|
if apply_fingerprint {
|
||||||
eprintln!("提示: M1 尚未实现设备指纹应用,已忽略 --apply-fingerprint。");
|
eprintln!("提示: M1 尚未实现设备指纹应用,已忽略 --apply-fingerprint。");
|
||||||
@@ -215,10 +235,15 @@ pub async fn switch_account(
|
|||||||
let target_home = paths::codex_home(codex_home)?;
|
let target_home = paths::codex_home(codex_home)?;
|
||||||
let mut store = Store::load(&config_home)?;
|
let mut store = Store::load(&config_home)?;
|
||||||
let account_id = find_unique_account_id(&store, query)?;
|
let account_id = find_unique_account_id(&store, query)?;
|
||||||
|
update_account_switch_options(&mut store, &account_id, &options)?;
|
||||||
switch_account_id(&mut store, &config_home, &target_home, &account_id).await
|
switch_account_id(&mut store, &config_home, &target_home, &account_id).await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn switch_auto(codex_home: Option<PathBuf>, apply_fingerprint: bool) -> Result<()> {
|
pub async fn switch_auto(
|
||||||
|
codex_home: Option<PathBuf>,
|
||||||
|
apply_fingerprint: bool,
|
||||||
|
options: SwitchOptions,
|
||||||
|
) -> Result<()> {
|
||||||
if apply_fingerprint {
|
if apply_fingerprint {
|
||||||
eprintln!("提示: M1 尚未实现设备指纹应用,已忽略 --apply-fingerprint。");
|
eprintln!("提示: M1 尚未实现设备指纹应用,已忽略 --apply-fingerprint。");
|
||||||
}
|
}
|
||||||
@@ -240,6 +265,7 @@ pub async fn switch_auto(codex_home: Option<PathBuf>, apply_fingerprint: bool) -
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
let account_id = best_auto_switch_account(&store)?;
|
let account_id = best_auto_switch_account(&store)?;
|
||||||
|
update_account_switch_options(&mut store, &account_id, &options)?;
|
||||||
switch_account_id(&mut store, &config_home, &target_home, &account_id).await?;
|
switch_account_id(&mut store, &config_home, &target_home, &account_id).await?;
|
||||||
if let Some(account) = store.find_account(&account_id) {
|
if let Some(account) = store.find_account(&account_id) {
|
||||||
println!(
|
println!(
|
||||||
@@ -424,7 +450,11 @@ fn account_from_auth(auth: auth_file::CodexAuthFile) -> Result<Account> {
|
|||||||
if auth_file::is_api_key_mode(&auth) {
|
if auth_file::is_api_key_mode(&auth) {
|
||||||
let key = auth_file::extract_api_key(&auth)
|
let key = auth_file::extract_api_key(&auth)
|
||||||
.ok_or_else(|| anyhow!("auth.json 缺少 OPENAI_API_KEY"))?;
|
.ok_or_else(|| anyhow!("auth.json 缺少 OPENAI_API_KEY"))?;
|
||||||
return api_key_account(key, auth_file::api_base_url(&auth));
|
return api_key_account(
|
||||||
|
key,
|
||||||
|
auth_file::api_base_url(&auth),
|
||||||
|
ApiKeyOptions::default(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let tokens = auth
|
let tokens = auth
|
||||||
@@ -488,6 +518,10 @@ fn oauth_account(tokens: Tokens, account_id_hint: Option<String>) -> Result<Acco
|
|||||||
id,
|
id,
|
||||||
email,
|
email,
|
||||||
auth_mode: AuthMode::Oauth,
|
auth_mode: AuthMode::Oauth,
|
||||||
|
alias: None,
|
||||||
|
model: None,
|
||||||
|
reasoning_effort: None,
|
||||||
|
api_provider_name: None,
|
||||||
plan_type,
|
plan_type,
|
||||||
account_id,
|
account_id,
|
||||||
organization_id,
|
organization_id,
|
||||||
@@ -503,12 +537,19 @@ fn oauth_account(tokens: Tokens, account_id_hint: Option<String>) -> Result<Acco
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn api_key_account(key: String, base_url: Option<String>) -> Result<Account> {
|
fn api_key_account(
|
||||||
|
key: String,
|
||||||
|
base_url: Option<String>,
|
||||||
|
options: ApiKeyOptions,
|
||||||
|
) -> Result<Account> {
|
||||||
let key = key.trim();
|
let key = key.trim();
|
||||||
if key.is_empty() {
|
if key.is_empty() {
|
||||||
return Err(anyhow!("API Key 不能为空"));
|
return Err(anyhow!("API Key 不能为空"));
|
||||||
}
|
}
|
||||||
let base_url = normalize_api_base_url(base_url);
|
let base_url = normalize_api_base_url(base_url);
|
||||||
|
let alias = normalize_optional_field(options.alias);
|
||||||
|
let model = normalize_optional_field(options.model);
|
||||||
|
let provider_name = normalize_optional_field(options.provider_name);
|
||||||
let id = stable_id("apikey", key, base_url.as_deref(), None);
|
let id = stable_id("apikey", key, base_url.as_deref(), None);
|
||||||
let email = api_base_url_display(base_url.as_deref()).to_string();
|
let email = api_base_url_display(base_url.as_deref()).to_string();
|
||||||
let now = Utc::now().timestamp();
|
let now = Utc::now().timestamp();
|
||||||
@@ -516,6 +557,10 @@ fn api_key_account(key: String, base_url: Option<String>) -> Result<Account> {
|
|||||||
id,
|
id,
|
||||||
email,
|
email,
|
||||||
auth_mode: AuthMode::ApiKey,
|
auth_mode: AuthMode::ApiKey,
|
||||||
|
alias,
|
||||||
|
model,
|
||||||
|
reasoning_effort: None,
|
||||||
|
api_provider_name: provider_name,
|
||||||
plan_type: None,
|
plan_type: None,
|
||||||
account_id: None,
|
account_id: None,
|
||||||
organization_id: None,
|
organization_id: None,
|
||||||
@@ -548,7 +593,11 @@ fn stable_id(kind: &str, a: &str, b: Option<&str>, c: Option<&str>) -> String {
|
|||||||
fn find_unique_account_id(store: &Store, query: &str) -> Result<String> {
|
fn find_unique_account_id(store: &Store, query: &str) -> Result<String> {
|
||||||
// Exact ids are always unambiguous. Email and prefix queries can match both
|
// Exact ids are always unambiguous. Email and prefix queries can match both
|
||||||
// personal and team accounts for the same login email, so reject ambiguity.
|
// personal and team accounts for the same login email, so reject ambiguity.
|
||||||
if let Some(account) = store.accounts.iter().find(|account| account.id == query) {
|
if let Some(account) = store
|
||||||
|
.accounts
|
||||||
|
.iter()
|
||||||
|
.find(|account| account.id == query || account.alias.as_deref() == Some(query))
|
||||||
|
{
|
||||||
return Ok(account.id.clone());
|
return Ok(account.id.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -559,6 +608,11 @@ fn find_unique_account_id(store: &Store, query: &str) -> Result<String> {
|
|||||||
.filter(|account| {
|
.filter(|account| {
|
||||||
account_id_matches_query(&account.id, query)
|
account_id_matches_query(&account.id, query)
|
||||||
|| account.email.eq_ignore_ascii_case(query)
|
|| account.email.eq_ignore_ascii_case(query)
|
||||||
|
|| account
|
||||||
|
.alias
|
||||||
|
.as_deref()
|
||||||
|
.map(|alias| alias.eq_ignore_ascii_case(query))
|
||||||
|
.unwrap_or(false)
|
||||||
|| account
|
|| account
|
||||||
.email
|
.email
|
||||||
.to_ascii_lowercase()
|
.to_ascii_lowercase()
|
||||||
@@ -589,6 +643,36 @@ fn find_unique_account_id(store: &Store, query: &str) -> Result<String> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn update_account_switch_options(
|
||||||
|
store: &mut Store,
|
||||||
|
account_id: &str,
|
||||||
|
options: &SwitchOptions,
|
||||||
|
) -> Result<()> {
|
||||||
|
let Some(account) = store.find_account_mut(account_id) else {
|
||||||
|
return Err(anyhow!("账号不存在: {account_id}"));
|
||||||
|
};
|
||||||
|
let mut changed = false;
|
||||||
|
if let Some(model) = normalize_optional_field(options.model.clone()) {
|
||||||
|
changed |= account.model.as_deref() != Some(model.as_str());
|
||||||
|
account.model = Some(model);
|
||||||
|
}
|
||||||
|
if let Some(reasoning_effort) = normalize_optional_field(options.reasoning_effort.clone()) {
|
||||||
|
changed |= account.reasoning_effort.as_deref() != Some(reasoning_effort.as_str());
|
||||||
|
account.reasoning_effort = Some(reasoning_effort);
|
||||||
|
}
|
||||||
|
if let Some(provider_name) = normalize_optional_field(options.provider_name.clone()) {
|
||||||
|
if account.auth_mode != AuthMode::ApiKey {
|
||||||
|
return Err(anyhow!("--name 仅支持 API Key 账号"));
|
||||||
|
}
|
||||||
|
changed |= account.api_provider_name.as_deref() != Some(provider_name.as_str());
|
||||||
|
account.api_provider_name = Some(provider_name);
|
||||||
|
}
|
||||||
|
if changed {
|
||||||
|
account.updated_at = Utc::now().timestamp();
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn account_id_matches_query(id: &str, query: &str) -> bool {
|
fn account_id_matches_query(id: &str, query: &str) -> bool {
|
||||||
id.starts_with(query)
|
id.starts_with(query)
|
||||||
|| id
|
|| id
|
||||||
@@ -682,7 +766,9 @@ fn account_plan_display(account: &Account) -> &str {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn account_email_display(account: &Account) -> &str {
|
fn account_email_display(account: &Account) -> &str {
|
||||||
if account.auth_mode == AuthMode::ApiKey {
|
if let Some(alias) = account.alias.as_deref() {
|
||||||
|
alias
|
||||||
|
} else if account.auth_mode == AuthMode::ApiKey {
|
||||||
api_base_url_display(account.api_base_url.as_deref())
|
api_base_url_display(account.api_base_url.as_deref())
|
||||||
} else {
|
} else {
|
||||||
&account.email
|
&account.email
|
||||||
@@ -804,8 +890,14 @@ fn shorten(value: &str, width: usize) -> String {
|
|||||||
|
|
||||||
fn print_account(account: &Account) {
|
fn print_account(account: &Account) {
|
||||||
println!("id: {}", account.id);
|
println!("id: {}", account.id);
|
||||||
|
println!("alias: {}", account.alias.as_deref().unwrap_or("-"));
|
||||||
println!("email: {}", account_email_display(account));
|
println!("email: {}", account_email_display(account));
|
||||||
println!("auth_mode: {}", auth_file::account_auth_mode_name(account));
|
println!("auth_mode: {}", auth_file::account_auth_mode_name(account));
|
||||||
|
println!("model: {}", account.model.as_deref().unwrap_or("-"));
|
||||||
|
println!(
|
||||||
|
"reasoning_effort: {}",
|
||||||
|
account.reasoning_effort.as_deref().unwrap_or("-")
|
||||||
|
);
|
||||||
println!("plan_type: {}", account_plan_display(account));
|
println!("plan_type: {}", account_plan_display(account));
|
||||||
println!(
|
println!(
|
||||||
"account_id: {}",
|
"account_id: {}",
|
||||||
@@ -820,6 +912,10 @@ fn print_account(account: &Account) {
|
|||||||
"api_base_url: {}",
|
"api_base_url: {}",
|
||||||
api_base_url_display(account.api_base_url.as_deref())
|
api_base_url_display(account.api_base_url.as_deref())
|
||||||
);
|
);
|
||||||
|
println!(
|
||||||
|
"api_provider_name: {}",
|
||||||
|
account.api_provider_name.as_deref().unwrap_or("-")
|
||||||
|
);
|
||||||
println!(
|
println!(
|
||||||
"openai_api_key: {}",
|
"openai_api_key: {}",
|
||||||
account
|
account
|
||||||
@@ -840,6 +936,14 @@ fn normalize_api_base_url(base_url: Option<String>) -> Option<String> {
|
|||||||
.map(|value| value.trim_end_matches('/').to_string())
|
.map(|value| value.trim_end_matches('/').to_string())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_optional_field(value: Option<String>) -> Option<String> {
|
||||||
|
value
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(ToOwned::to_owned)
|
||||||
|
}
|
||||||
|
|
||||||
fn mask_api_key(key: &str) -> String {
|
fn mask_api_key(key: &str) -> String {
|
||||||
let chars = key.chars().collect::<Vec<_>>();
|
let chars = key.chars().collect::<Vec<_>>();
|
||||||
if chars.len() <= 8 {
|
if chars.len() <= 8 {
|
||||||
|
|||||||
+29
@@ -58,6 +58,12 @@ pub enum Commands {
|
|||||||
codex_home: Option<PathBuf>,
|
codex_home: Option<PathBuf>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
apply_fingerprint: bool,
|
apply_fingerprint: bool,
|
||||||
|
#[arg(long)]
|
||||||
|
model: Option<String>,
|
||||||
|
#[arg(long, alias = "reasoning-effort")]
|
||||||
|
effort: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
name: Option<String>,
|
||||||
},
|
},
|
||||||
/// Prepare auth, set CODEX_HOME, and execute a command.
|
/// Prepare auth, set CODEX_HOME, and execute a command.
|
||||||
Run(RunArgs),
|
Run(RunArgs),
|
||||||
@@ -102,6 +108,17 @@ pub struct LoginArgs {
|
|||||||
/// Start OpenAI OAuth login for Codex.
|
/// Start OpenAI OAuth login for Codex.
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Option<LoginCommands>,
|
pub command: Option<LoginCommands>,
|
||||||
|
/// Add an API key account directly, for example: cdxs login --api sk-...
|
||||||
|
#[arg(long, value_name = "KEY")]
|
||||||
|
pub api: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
pub base_url: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
pub alias: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
pub model: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
pub name: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub manual: bool,
|
pub manual: bool,
|
||||||
#[arg(long, default_value_t = 1455)]
|
#[arg(long, default_value_t = 1455)]
|
||||||
@@ -141,6 +158,12 @@ pub enum LoginCommands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
base_url: Option<String>,
|
base_url: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
alias: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
model: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
name: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
switch: bool,
|
switch: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -184,6 +207,12 @@ pub enum AccountCommands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
base_url: Option<String>,
|
base_url: Option<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
|
alias: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
model: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
name: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
switch: bool,
|
switch: bool,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+73
-32
@@ -14,52 +14,64 @@ const MANAGED_PROVIDER_PREFIX: &str = "cdxs_api_";
|
|||||||
|
|
||||||
pub fn apply_account_provider(codex_home: &Path, account: &Account) -> Result<()> {
|
pub fn apply_account_provider(codex_home: &Path, account: &Account) -> Result<()> {
|
||||||
match account.auth_mode {
|
match account.auth_mode {
|
||||||
AuthMode::Oauth => clear_managed_provider(codex_home),
|
AuthMode::Oauth => apply_oauth_config(codex_home, account),
|
||||||
AuthMode::ApiKey => apply_api_key_provider(codex_home, account),
|
AuthMode::ApiKey => apply_api_key_provider(codex_home, account),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_api_key_provider(codex_home: &Path, account: &Account) -> Result<()> {
|
fn apply_oauth_config(codex_home: &Path, account: &Account) -> Result<()> {
|
||||||
let path = paths::codex_config_path(codex_home);
|
let path = paths::codex_config_path(codex_home);
|
||||||
let mut config = read_config(&path)?;
|
if !path.exists() && account.model.is_none() && account.reasoning_effort.is_none() {
|
||||||
let mut changed = remove_previous_managed_provider(&mut config);
|
return Ok(());
|
||||||
|
|
||||||
if let Some(base_url) = account.api_base_url.as_deref().and_then(normalize_base_url) {
|
|
||||||
let provider_id = provider_id(account, &base_url);
|
|
||||||
config.insert(
|
|
||||||
"model_provider".to_string(),
|
|
||||||
Value::String(provider_id.clone()),
|
|
||||||
);
|
|
||||||
|
|
||||||
let providers = table_entry(&mut config, "model_providers");
|
|
||||||
let mut provider = Map::new();
|
|
||||||
provider.insert("name".to_string(), Value::String("OpenAI".to_string()));
|
|
||||||
provider.insert("base_url".to_string(), Value::String(base_url));
|
|
||||||
provider.insert(
|
|
||||||
"wire_api".to_string(),
|
|
||||||
Value::String("responses".to_string()),
|
|
||||||
);
|
|
||||||
provider.insert("requires_openai_auth".to_string(), Value::Boolean(true));
|
|
||||||
providers.insert(provider_id, Value::Table(provider));
|
|
||||||
changed = true;
|
|
||||||
}
|
}
|
||||||
|
let mut config = read_config(&path)?;
|
||||||
|
let mut changed = apply_common_model_settings(&mut config, account);
|
||||||
|
changed |= remove_previous_managed_provider(&mut config);
|
||||||
if changed {
|
if changed {
|
||||||
write_config(&path, codex_home, &config)?;
|
write_config(&path, codex_home, &config)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn clear_managed_provider(codex_home: &Path) -> Result<()> {
|
fn apply_api_key_provider(codex_home: &Path, account: &Account) -> Result<()> {
|
||||||
let path = paths::codex_config_path(codex_home);
|
let path = paths::codex_config_path(codex_home);
|
||||||
if !path.exists() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let mut config = read_config(&path)?;
|
let mut config = read_config(&path)?;
|
||||||
if remove_previous_managed_provider(&mut config) {
|
remove_previous_managed_provider(&mut config);
|
||||||
write_config(&path, codex_home, &config)?;
|
|
||||||
}
|
apply_common_model_settings(&mut config, account);
|
||||||
Ok(())
|
|
||||||
|
let base_url = account
|
||||||
|
.api_base_url
|
||||||
|
.as_deref()
|
||||||
|
.and_then(normalize_base_url)
|
||||||
|
.unwrap_or_else(|| "https://api.openai.com/v1".to_string());
|
||||||
|
let provider_id = provider_id(account, &base_url);
|
||||||
|
config.insert(
|
||||||
|
"model_provider".to_string(),
|
||||||
|
Value::String(provider_id.clone()),
|
||||||
|
);
|
||||||
|
|
||||||
|
let providers = table_entry(&mut config, "model_providers");
|
||||||
|
let mut provider = Map::new();
|
||||||
|
provider.insert(
|
||||||
|
"name".to_string(),
|
||||||
|
Value::String(
|
||||||
|
account
|
||||||
|
.api_provider_name
|
||||||
|
.as_deref()
|
||||||
|
.and_then(normalize_field)
|
||||||
|
.unwrap_or_else(|| "OpenAI".to_string()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
provider.insert("base_url".to_string(), Value::String(base_url));
|
||||||
|
provider.insert(
|
||||||
|
"wire_api".to_string(),
|
||||||
|
Value::String("responses".to_string()),
|
||||||
|
);
|
||||||
|
provider.insert("requires_openai_auth".to_string(), Value::Boolean(true));
|
||||||
|
providers.insert(provider_id, Value::Table(provider));
|
||||||
|
|
||||||
|
write_config(&path, codex_home, &config)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_config(path: &Path) -> Result<Map<String, Value>> {
|
fn read_config(path: &Path) -> Result<Map<String, Value>> {
|
||||||
@@ -131,3 +143,32 @@ fn normalize_base_url(value: &str) -> Option<String> {
|
|||||||
Some(value.to_string())
|
Some(value.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_common_model_settings(config: &mut Map<String, Value>, account: &Account) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
|
if let Some(model) = account.model.as_deref().and_then(normalize_field) {
|
||||||
|
config.insert("model".to_string(), Value::String(model));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
if let Some(reasoning_effort) = account
|
||||||
|
.reasoning_effort
|
||||||
|
.as_deref()
|
||||||
|
.and_then(normalize_field)
|
||||||
|
{
|
||||||
|
config.insert(
|
||||||
|
"model_reasoning_effort".to_string(),
|
||||||
|
Value::String(reasoning_effort),
|
||||||
|
);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_field(value: &str) -> Option<String> {
|
||||||
|
let value = value.trim();
|
||||||
|
if value.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -56,6 +56,14 @@ pub struct Account {
|
|||||||
pub email: String,
|
pub email: String,
|
||||||
pub auth_mode: AuthMode,
|
pub auth_mode: AuthMode,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
pub alias: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub model: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub reasoning_effort: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub api_provider_name: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
pub plan_type: Option<String>,
|
pub plan_type: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub account_id: Option<String>,
|
pub account_id: Option<String>,
|
||||||
@@ -234,6 +242,11 @@ impl Store {
|
|||||||
.split_once('_')
|
.split_once('_')
|
||||||
.map(|(_, suffix)| suffix.starts_with(query))
|
.map(|(_, suffix)| suffix.starts_with(query))
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
|
|| account
|
||||||
|
.alias
|
||||||
|
.as_deref()
|
||||||
|
.map(|alias| alias.eq_ignore_ascii_case(query))
|
||||||
|
.unwrap_or(false)
|
||||||
|| account.email.eq_ignore_ascii_case(query)
|
|| account.email.eq_ignore_ascii_case(query)
|
||||||
|| account
|
|| account
|
||||||
.email
|
.email
|
||||||
|
|||||||
+52
-4
@@ -44,9 +44,36 @@ async fn main() -> Result<()> {
|
|||||||
Some(LoginCommands::Api {
|
Some(LoginCommands::Api {
|
||||||
key,
|
key,
|
||||||
base_url,
|
base_url,
|
||||||
|
alias,
|
||||||
|
model,
|
||||||
|
name,
|
||||||
switch,
|
switch,
|
||||||
}) => account::add_api_key(key, base_url, switch),
|
}) => account::add_api_key(
|
||||||
None => oauth::login_oauth(args.manual, args.port, args.switch).await,
|
key,
|
||||||
|
base_url,
|
||||||
|
account::ApiKeyOptions {
|
||||||
|
alias,
|
||||||
|
model,
|
||||||
|
provider_name: name,
|
||||||
|
},
|
||||||
|
switch,
|
||||||
|
),
|
||||||
|
None => {
|
||||||
|
if let Some(key) = args.api {
|
||||||
|
account::add_api_key(
|
||||||
|
key,
|
||||||
|
args.base_url,
|
||||||
|
account::ApiKeyOptions {
|
||||||
|
alias: args.alias,
|
||||||
|
model: args.model,
|
||||||
|
provider_name: args.name,
|
||||||
|
},
|
||||||
|
args.switch,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
oauth::login_oauth(args.manual, args.port, args.switch).await
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Commands::Import(args) => match args.command {
|
Commands::Import(args) => match args.command {
|
||||||
Some(ImportCommands::Auth {
|
Some(ImportCommands::Auth {
|
||||||
@@ -66,9 +93,17 @@ async fn main() -> Result<()> {
|
|||||||
auto,
|
auto,
|
||||||
codex_home,
|
codex_home,
|
||||||
apply_fingerprint,
|
apply_fingerprint,
|
||||||
|
model,
|
||||||
|
effort,
|
||||||
|
name,
|
||||||
} => {
|
} => {
|
||||||
|
let options = account::SwitchOptions {
|
||||||
|
model,
|
||||||
|
reasoning_effort: effort,
|
||||||
|
provider_name: name,
|
||||||
|
};
|
||||||
if auto || account.is_none() {
|
if auto || account.is_none() {
|
||||||
account::switch_auto(codex_home, apply_fingerprint).await
|
account::switch_auto(codex_home, apply_fingerprint, options).await
|
||||||
} else {
|
} else {
|
||||||
account::switch_account(
|
account::switch_account(
|
||||||
account.as_deref().ok_or_else(|| {
|
account.as_deref().ok_or_else(|| {
|
||||||
@@ -76,6 +111,7 @@ async fn main() -> Result<()> {
|
|||||||
})?,
|
})?,
|
||||||
codex_home,
|
codex_home,
|
||||||
apply_fingerprint,
|
apply_fingerprint,
|
||||||
|
options,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -109,8 +145,20 @@ async fn main() -> Result<()> {
|
|||||||
AccountCommands::AddApiKey {
|
AccountCommands::AddApiKey {
|
||||||
key,
|
key,
|
||||||
base_url,
|
base_url,
|
||||||
|
alias,
|
||||||
|
model,
|
||||||
|
name,
|
||||||
switch,
|
switch,
|
||||||
} => account::add_api_key(key, base_url, switch),
|
} => account::add_api_key(
|
||||||
|
key,
|
||||||
|
base_url,
|
||||||
|
account::ApiKeyOptions {
|
||||||
|
alias,
|
||||||
|
model,
|
||||||
|
provider_name: name,
|
||||||
|
},
|
||||||
|
switch,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
Commands::Home { command } => match command {
|
Commands::Home { command } => match command {
|
||||||
HomeCommands::List { json } => account::list_homes(json),
|
HomeCommands::List { json } => account::list_homes(json),
|
||||||
|
|||||||
+14
-4
@@ -148,8 +148,8 @@ pub async fn remote(json: bool) -> Result<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
println!(
|
println!(
|
||||||
"{:<22} {:<34} {:<10} {:<12} {}",
|
"{:<22} {:<16} {:<34} {:<10} {:<18} {:<12} {}",
|
||||||
"ID", "Email", "Mode", "Plan", "Quota"
|
"ID", "Alias", "Email", "Mode", "Model", "Provider", "Quota"
|
||||||
);
|
);
|
||||||
for account in &remote_state.accounts {
|
for account in &remote_state.accounts {
|
||||||
let quota = account
|
let quota = account
|
||||||
@@ -163,11 +163,13 @@ pub async fn remote(json: bool) -> Result<()> {
|
|||||||
})
|
})
|
||||||
.unwrap_or_else(|| "-".to_string());
|
.unwrap_or_else(|| "-".to_string());
|
||||||
println!(
|
println!(
|
||||||
"{:<22} {:<34} {:<10} {:<12} {}",
|
"{:<22} {:<16} {:<34} {:<10} {:<18} {:<12} {}",
|
||||||
shorten(&account.id, 22),
|
shorten(&account.id, 22),
|
||||||
|
shorten(account.alias.as_deref().unwrap_or("-"), 16),
|
||||||
shorten(account_email_display(account), 34),
|
shorten(account_email_display(account), 34),
|
||||||
account_auth_mode_name(account),
|
account_auth_mode_name(account),
|
||||||
account_plan_display(account),
|
shorten(account.model.as_deref().unwrap_or("-"), 18),
|
||||||
|
shorten(account_provider_display(account), 12),
|
||||||
quota
|
quota
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -308,6 +310,14 @@ fn account_plan_display(account: &Account) -> &str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn account_provider_display(account: &Account) -> &str {
|
||||||
|
if account.auth_mode == crate::config_store::AuthMode::ApiKey {
|
||||||
|
account.api_provider_name.as_deref().unwrap_or("-")
|
||||||
|
} else {
|
||||||
|
account_plan_display(account)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn shorten(value: &str, width: usize) -> String {
|
fn shorten(value: &str, width: usize) -> String {
|
||||||
if value.chars().count() <= width {
|
if value.chars().count() <= width {
|
||||||
return value.to_string();
|
return value.to_string();
|
||||||
|
|||||||
Reference in New Issue
Block a user