Add API key login mode
This commit is contained in:
Generated
+1
-1
@@ -232,7 +232,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cdxs"
|
name = "cdxs"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cdxs"
|
name = "cdxs"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Codex account switcher CLI"
|
description = "Codex account switcher CLI"
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
Codex 的认证和会话状态都来自 `CODEX_HOME`。`cdxs` 做的是在这个目录外面加一层可操作的管理能力:
|
Codex 的认证和会话状态都来自 `CODEX_HOME`。`cdxs` 做的是在这个目录外面加一层可操作的管理能力:
|
||||||
|
|
||||||
- 账号管理:导入已有 `auth.json`,通过 OAuth 登录,添加 API Key 账号,切换当前账号,并在需要时刷新 OAuth token。
|
- 账号管理:导入已有 `auth.json`,通过 OAuth 或 API Key 登录,切换当前账号,并在需要时刷新 OAuth token。
|
||||||
- 配额查看:查询 OAuth 账号的 Codex 使用配额,并缓存到本地。
|
- 配额查看:查询 OAuth 账号的 Codex 使用配额,并缓存到本地。
|
||||||
- Home 管理:创建命名的 `CODEX_HOME`,并绑定到指定账号。
|
- Home 管理:创建命名的 `CODEX_HOME`,并绑定到指定账号。
|
||||||
- 命令运行:用指定账号或 home 启动子进程,只为该进程设置 `CODEX_HOME`。
|
- 命令运行:用指定账号或 home 启动子进程,只为该进程设置 `CODEX_HOME`。
|
||||||
@@ -43,6 +43,7 @@ Codex home 的解析顺序:
|
|||||||
- `cdxs show`:列出账号,但不请求配额接口。
|
- `cdxs show`:列出账号,但不请求配额接口。
|
||||||
- `cdxs switch`:不带参数时自动选择可用配额最优的账号。
|
- `cdxs switch`:不带参数时自动选择可用配额最优的账号。
|
||||||
- `cdxs switch <账号>`:切换到指定账号。
|
- `cdxs switch <账号>`:切换到指定账号。
|
||||||
|
- `cdxs login api --key <key> --base-url <url> --switch`:保存 API Key 账号并可选切换;`base-url` 为空时使用 OpenAI 默认 API。
|
||||||
- `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`。
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
- 保存和管理多个 Codex OAuth 账号。
|
- 保存和管理多个 Codex OAuth 账号。
|
||||||
- 保存和管理多个 Codex API Key 账号。
|
- 保存和管理多个 Codex API Key 账号。
|
||||||
|
- 通过 `login api` 保存 API Key 账号,可绑定自定义 API base URL。
|
||||||
- 从现有 Codex `auth.json` 导入账号。
|
- 从现有 Codex `auth.json` 导入账号。
|
||||||
- 通过 OpenAI OAuth 登录并保存 Codex token。
|
- 通过 OpenAI OAuth 登录并保存 Codex token。
|
||||||
- 将指定账号切换写入 Codex `auth.json`。
|
- 将指定账号切换写入 Codex `auth.json`。
|
||||||
|
|||||||
+71
-10
@@ -12,7 +12,9 @@ use sha2::{Digest, Sha256};
|
|||||||
|
|
||||||
use crate::auth_file;
|
use crate::auth_file;
|
||||||
use crate::config_store::{self, Account, AuthMode, Home, Store, Tokens};
|
use crate::config_store::{self, Account, AuthMode, Home, Store, Tokens};
|
||||||
use crate::{jwt, paths, token};
|
use crate::{codex_config, jwt, paths, token};
|
||||||
|
|
||||||
|
const DEFAULT_API_BASE_URL: &str = "https://api.openai.com/v1";
|
||||||
|
|
||||||
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
|
||||||
@@ -30,6 +32,7 @@ pub fn import_auth(file: Option<PathBuf>, codex_home: Option<PathBuf>, switch: b
|
|||||||
store.meta.current_account_id = Some(id.clone());
|
store.meta.current_account_id = Some(id.clone());
|
||||||
let account = store.find_account(&id).expect("just inserted account");
|
let account = store.find_account(&id).expect("just inserted account");
|
||||||
auth_file::write_account_to_auth(&paths::auth_path(&source_home), &source_home, account)?;
|
auth_file::write_account_to_auth(&paths::auth_path(&source_home), &source_home, account)?;
|
||||||
|
codex_config::apply_account_provider(&source_home, account)?;
|
||||||
}
|
}
|
||||||
store.save(&config_home)?;
|
store.save(&config_home)?;
|
||||||
println!("已导入账号: {email} ({id})");
|
println!("已导入账号: {email} ({id})");
|
||||||
@@ -47,6 +50,7 @@ pub fn add_api_key(key: String, base_url: Option<String>, switch: bool) -> Resul
|
|||||||
store.meta.current_account_id = Some(id.clone());
|
store.meta.current_account_id = Some(id.clone());
|
||||||
let account = store.find_account(&id).expect("just inserted account");
|
let account = store.find_account(&id).expect("just inserted account");
|
||||||
auth_file::write_account_to_auth(&paths::auth_path(&home), &home, account)?;
|
auth_file::write_account_to_auth(&paths::auth_path(&home), &home, account)?;
|
||||||
|
codex_config::apply_account_provider(&home, account)?;
|
||||||
}
|
}
|
||||||
store.save(&home)?;
|
store.save(&home)?;
|
||||||
println!("已保存 API Key 账号: {email} ({id})");
|
println!("已保存 API Key 账号: {email} ({id})");
|
||||||
@@ -119,7 +123,7 @@ fn print_accounts(store: &Store, json: bool) -> Result<()> {
|
|||||||
print_account_table_row(
|
print_account_table_row(
|
||||||
current,
|
current,
|
||||||
&account.id,
|
&account.id,
|
||||||
&account.email,
|
account_email_display(account),
|
||||||
auth_file::account_auth_mode_name(account),
|
auth_file::account_auth_mode_name(account),
|
||||||
account_plan_display(account),
|
account_plan_display(account),
|
||||||
&primary_quota,
|
&primary_quota,
|
||||||
@@ -247,6 +251,7 @@ async fn switch_account_id(
|
|||||||
.find_account(account_id)
|
.find_account(account_id)
|
||||||
.ok_or_else(|| anyhow!("账号不存在: {account_id}"))?;
|
.ok_or_else(|| anyhow!("账号不存在: {account_id}"))?;
|
||||||
auth_file::write_account_to_auth(&paths::auth_path(target_home), target_home, account)?;
|
auth_file::write_account_to_auth(&paths::auth_path(target_home), target_home, account)?;
|
||||||
|
codex_config::apply_account_provider(target_home, account)?;
|
||||||
if let Some(account) = store.find_account_mut(account_id) {
|
if let Some(account) = store.find_account_mut(account_id) {
|
||||||
account.last_used_at = Utc::now().timestamp();
|
account.last_used_at = Utc::now().timestamp();
|
||||||
}
|
}
|
||||||
@@ -305,6 +310,7 @@ pub fn create_home(name: &str, path: PathBuf, account: Option<String>) -> Result
|
|||||||
if let Some(account_id) = bound_account_id.as_deref() {
|
if let Some(account_id) = bound_account_id.as_deref() {
|
||||||
let account = store.find_account(account_id).expect("checked account");
|
let account = store.find_account(account_id).expect("checked account");
|
||||||
auth_file::write_account_to_auth(&paths::auth_path(&path), &path, account)?;
|
auth_file::write_account_to_auth(&paths::auth_path(&path), &path, account)?;
|
||||||
|
codex_config::apply_account_provider(&path, account)?;
|
||||||
}
|
}
|
||||||
store.homes.push(Home {
|
store.homes.push(Home {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
@@ -377,6 +383,7 @@ pub async fn prepare_account_in_home(query: &str, codex_home: PathBuf) -> Result
|
|||||||
.find_account(&account_id)
|
.find_account(&account_id)
|
||||||
.ok_or_else(|| anyhow!("账号不存在: {account_id}"))?;
|
.ok_or_else(|| anyhow!("账号不存在: {account_id}"))?;
|
||||||
auth_file::write_account_to_auth(&paths::auth_path(&codex_home), &codex_home, account)?;
|
auth_file::write_account_to_auth(&paths::auth_path(&codex_home), &codex_home, account)?;
|
||||||
|
codex_config::apply_account_provider(&codex_home, account)?;
|
||||||
(account.id.clone(), account.email.clone())
|
(account.id.clone(), account.email.clone())
|
||||||
};
|
};
|
||||||
if let Some(account) = store.find_account_mut(&account_id) {
|
if let Some(account) = store.find_account_mut(&account_id) {
|
||||||
@@ -488,14 +495,15 @@ fn api_key_account(key: String, base_url: Option<String>) -> Result<Account> {
|
|||||||
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 id = stable_id("apikey", key, base_url.as_deref(), None);
|
let id = stable_id("apikey", key, base_url.as_deref(), None);
|
||||||
let email = format!("api-key-{}", &id[id.len().saturating_sub(8)..]);
|
let email = api_base_url_display(base_url.as_deref()).to_string();
|
||||||
let now = Utc::now().timestamp();
|
let now = Utc::now().timestamp();
|
||||||
Ok(Account {
|
Ok(Account {
|
||||||
id,
|
id,
|
||||||
email,
|
email,
|
||||||
auth_mode: AuthMode::ApiKey,
|
auth_mode: AuthMode::ApiKey,
|
||||||
plan_type: Some("API_KEY".to_string()),
|
plan_type: None,
|
||||||
account_id: None,
|
account_id: None,
|
||||||
organization_id: None,
|
organization_id: None,
|
||||||
tokens: None,
|
tokens: None,
|
||||||
@@ -634,13 +642,27 @@ fn format_quota(account: &Account) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn account_plan_display(account: &Account) -> &str {
|
fn account_plan_display(account: &Account) -> &str {
|
||||||
if account.requires_reauth {
|
if account.auth_mode == AuthMode::ApiKey {
|
||||||
|
"-"
|
||||||
|
} else if account.requires_reauth {
|
||||||
"reauth"
|
"reauth"
|
||||||
} else {
|
} else {
|
||||||
account.plan_type.as_deref().unwrap_or("-")
|
account.plan_type.as_deref().unwrap_or("-")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn account_email_display(account: &Account) -> &str {
|
||||||
|
if account.auth_mode == AuthMode::ApiKey {
|
||||||
|
api_base_url_display(account.api_base_url.as_deref())
|
||||||
|
} else {
|
||||||
|
&account.email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn api_base_url_display(base_url: Option<&str>) -> &str {
|
||||||
|
base_url.unwrap_or(DEFAULT_API_BASE_URL)
|
||||||
|
}
|
||||||
|
|
||||||
fn format_quota_cells(account: &Account) -> (String, String) {
|
fn format_quota_cells(account: &Account) -> (String, String) {
|
||||||
let Some(quota) = account.quota.as_ref() else {
|
let Some(quota) = account.quota.as_ref() else {
|
||||||
return ("-".to_string(), "-".to_string());
|
return ("-".to_string(), "-".to_string());
|
||||||
@@ -710,7 +732,7 @@ fn print_account_table_border() {
|
|||||||
"-".repeat(3),
|
"-".repeat(3),
|
||||||
"-".repeat(24),
|
"-".repeat(24),
|
||||||
"-".repeat(30),
|
"-".repeat(30),
|
||||||
"-".repeat(8),
|
"-".repeat(10),
|
||||||
"-".repeat(8),
|
"-".repeat(8),
|
||||||
"-".repeat(14),
|
"-".repeat(14),
|
||||||
"-".repeat(14)
|
"-".repeat(14)
|
||||||
@@ -727,11 +749,11 @@ fn print_account_table_row(
|
|||||||
secondary_quota: &str,
|
secondary_quota: &str,
|
||||||
) {
|
) {
|
||||||
println!(
|
println!(
|
||||||
"| {:<1} | {:<22} | {:<28} | {:<6} | {:<6} | {:<12} | {:<12} |",
|
"| {:<1} | {:<22} | {:<28} | {:<8} | {:<6} | {:<12} | {:<12} |",
|
||||||
shorten(marker, 1),
|
shorten(marker, 1),
|
||||||
shorten(id, 22),
|
shorten(id, 22),
|
||||||
shorten(email, 28),
|
shorten(email, 28),
|
||||||
shorten(mode, 6),
|
shorten(mode, 8),
|
||||||
shorten(plan, 6),
|
shorten(plan, 6),
|
||||||
shorten(primary_quota, 12),
|
shorten(primary_quota, 12),
|
||||||
shorten(secondary_quota, 12)
|
shorten(secondary_quota, 12)
|
||||||
@@ -752,9 +774,9 @@ 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!("email: {}", account.email);
|
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!("plan_type: {}", account.plan_type.as_deref().unwrap_or("-"));
|
println!("plan_type: {}", account_plan_display(account));
|
||||||
println!(
|
println!(
|
||||||
"account_id: {}",
|
"account_id: {}",
|
||||||
account.account_id.as_deref().unwrap_or("-")
|
account.account_id.as_deref().unwrap_or("-")
|
||||||
@@ -763,5 +785,44 @@ fn print_account(account: &Account) {
|
|||||||
"organization_id: {}",
|
"organization_id: {}",
|
||||||
account.organization_id.as_deref().unwrap_or("-")
|
account.organization_id.as_deref().unwrap_or("-")
|
||||||
);
|
);
|
||||||
|
if account.auth_mode == AuthMode::ApiKey {
|
||||||
|
println!(
|
||||||
|
"api_base_url: {}",
|
||||||
|
api_base_url_display(account.api_base_url.as_deref())
|
||||||
|
);
|
||||||
|
println!(
|
||||||
|
"openai_api_key: {}",
|
||||||
|
account
|
||||||
|
.openai_api_key
|
||||||
|
.as_deref()
|
||||||
|
.map(mask_api_key)
|
||||||
|
.unwrap_or_else(|| "-".to_string())
|
||||||
|
);
|
||||||
|
}
|
||||||
println!("requires_reauth: {}", account.requires_reauth);
|
println!("requires_reauth: {}", account.requires_reauth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn normalize_api_base_url(base_url: Option<String>) -> Option<String> {
|
||||||
|
base_url
|
||||||
|
.as_deref()
|
||||||
|
.map(str::trim)
|
||||||
|
.filter(|value| !value.is_empty())
|
||||||
|
.map(|value| value.trim_end_matches('/').to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mask_api_key(key: &str) -> String {
|
||||||
|
let chars = key.chars().collect::<Vec<_>>();
|
||||||
|
if chars.len() <= 8 {
|
||||||
|
return "<stored>".to_string();
|
||||||
|
}
|
||||||
|
let prefix = chars.iter().take(3).collect::<String>();
|
||||||
|
let suffix = chars
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.take(4)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.into_iter()
|
||||||
|
.rev()
|
||||||
|
.collect::<String>();
|
||||||
|
format!("{prefix}...{suffix}")
|
||||||
|
}
|
||||||
|
|||||||
@@ -132,6 +132,15 @@ pub enum LoginCommands {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
switch: bool,
|
switch: bool,
|
||||||
},
|
},
|
||||||
|
/// Add an OpenAI API key account.
|
||||||
|
Api {
|
||||||
|
#[arg(long)]
|
||||||
|
key: String,
|
||||||
|
#[arg(long)]
|
||||||
|
base_url: Option<String>,
|
||||||
|
#[arg(long)]
|
||||||
|
switch: bool,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
//! Codex `config.toml` helpers for account-specific provider settings.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
use toml::map::Map;
|
||||||
|
use toml::Value;
|
||||||
|
|
||||||
|
use crate::config_store::{Account, AuthMode};
|
||||||
|
use crate::{atomic, paths};
|
||||||
|
|
||||||
|
const MANAGED_PROVIDER_PREFIX: &str = "cdxs_api_";
|
||||||
|
|
||||||
|
pub fn apply_account_provider(codex_home: &Path, account: &Account) -> Result<()> {
|
||||||
|
match account.auth_mode {
|
||||||
|
AuthMode::Oauth => clear_managed_provider(codex_home),
|
||||||
|
AuthMode::ApiKey => apply_api_key_provider(codex_home, account),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_api_key_provider(codex_home: &Path, account: &Account) -> Result<()> {
|
||||||
|
let path = paths::codex_config_path(codex_home);
|
||||||
|
let mut config = read_config(&path)?;
|
||||||
|
let mut changed = remove_previous_managed_provider(&mut config);
|
||||||
|
|
||||||
|
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("cdxs api".to_string()));
|
||||||
|
provider.insert("base_url".to_string(), Value::String(base_url));
|
||||||
|
provider.insert("requires_openai_auth".to_string(), Value::Boolean(true));
|
||||||
|
providers.insert(provider_id, Value::Table(provider));
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if changed {
|
||||||
|
write_config(&path, codex_home, &config)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clear_managed_provider(codex_home: &Path) -> Result<()> {
|
||||||
|
let path = paths::codex_config_path(codex_home);
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let mut config = read_config(&path)?;
|
||||||
|
if remove_previous_managed_provider(&mut config) {
|
||||||
|
write_config(&path, codex_home, &config)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_config(path: &Path) -> Result<Map<String, Value>> {
|
||||||
|
if !path.exists() {
|
||||||
|
return Ok(Map::new());
|
||||||
|
}
|
||||||
|
let content = std::fs::read_to_string(path)
|
||||||
|
.with_context(|| format!("读取 Codex config.toml 失败: {}", path.display()))?;
|
||||||
|
if content.trim().is_empty() {
|
||||||
|
return Ok(Map::new());
|
||||||
|
}
|
||||||
|
let value: Value = toml::from_str(&content)
|
||||||
|
.with_context(|| format!("解析 Codex config.toml 失败: {}", path.display()))?;
|
||||||
|
Ok(value.as_table().cloned().unwrap_or_default())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_config(path: &Path, codex_home: &Path, config: &Map<String, Value>) -> Result<()> {
|
||||||
|
atomic::backup_if_exists(path, codex_home, "config.toml")?;
|
||||||
|
let content = toml::to_string_pretty(config).context("序列化 Codex config.toml 失败")?;
|
||||||
|
atomic::write_atomic(path, &content)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_previous_managed_provider(config: &mut Map<String, Value>) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
|
let managed_current = config
|
||||||
|
.get("model_provider")
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.filter(|provider| provider.starts_with(MANAGED_PROVIDER_PREFIX))
|
||||||
|
.map(ToOwned::to_owned);
|
||||||
|
|
||||||
|
if managed_current.is_some() {
|
||||||
|
config.remove("model_provider");
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(Value::Table(providers)) = config.get_mut("model_providers") {
|
||||||
|
let before = providers.len();
|
||||||
|
providers.retain(|key, _| !key.starts_with(MANAGED_PROVIDER_PREFIX));
|
||||||
|
changed |= providers.len() != before;
|
||||||
|
}
|
||||||
|
changed
|
||||||
|
}
|
||||||
|
|
||||||
|
fn table_entry<'a>(config: &'a mut Map<String, Value>, key: &str) -> &'a mut Map<String, Value> {
|
||||||
|
let needs_table = !matches!(config.get(key), Some(Value::Table(_)));
|
||||||
|
if needs_table {
|
||||||
|
config.insert(key.to_string(), Value::Table(Map::new()));
|
||||||
|
}
|
||||||
|
config
|
||||||
|
.get_mut(key)
|
||||||
|
.and_then(Value::as_table_mut)
|
||||||
|
.expect("table entry was just inserted")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn provider_id(account: &Account, base_url: &str) -> String {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(account.id.as_bytes());
|
||||||
|
hasher.update([0]);
|
||||||
|
hasher.update(base_url.as_bytes());
|
||||||
|
let hex = hex::encode(hasher.finalize());
|
||||||
|
format!("{MANAGED_PROVIDER_PREFIX}{}", &hex[..12])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn normalize_base_url(value: &str) -> Option<String> {
|
||||||
|
let value = value.trim().trim_end_matches('/');
|
||||||
|
if value.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some(value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ mod account;
|
|||||||
mod atomic;
|
mod atomic;
|
||||||
mod auth_file;
|
mod auth_file;
|
||||||
mod cli;
|
mod cli;
|
||||||
|
mod codex_config;
|
||||||
mod config_store;
|
mod config_store;
|
||||||
mod http_client;
|
mod http_client;
|
||||||
mod jwt;
|
mod jwt;
|
||||||
@@ -40,6 +41,11 @@ async fn main() -> Result<()> {
|
|||||||
port,
|
port,
|
||||||
switch,
|
switch,
|
||||||
}) => oauth::login_oauth(manual, port, switch).await,
|
}) => oauth::login_oauth(manual, port, switch).await,
|
||||||
|
Some(LoginCommands::Api {
|
||||||
|
key,
|
||||||
|
base_url,
|
||||||
|
switch,
|
||||||
|
}) => account::add_api_key(key, base_url, switch),
|
||||||
None => oauth::login_oauth(args.manual, args.port, args.switch).await,
|
None => oauth::login_oauth(args.manual, args.port, args.switch).await,
|
||||||
},
|
},
|
||||||
Commands::Import(args) => match args.command {
|
Commands::Import(args) => match args.command {
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ pub fn auth_path(codex_home: &std::path::Path) -> PathBuf {
|
|||||||
codex_home.join("auth.json")
|
codex_home.join("auth.json")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn codex_config_path(codex_home: &std::path::Path) -> PathBuf {
|
||||||
|
codex_home.join("config.toml")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn expand_home(path: PathBuf) -> PathBuf {
|
pub fn expand_home(path: PathBuf) -> PathBuf {
|
||||||
// PathBuf does not expand ~ on Windows or Unix, so handle the common cases.
|
// PathBuf does not expand ~ on Windows or Unix, so handle the common cases.
|
||||||
let raw = path.to_string_lossy();
|
let raw = path.to_string_lossy();
|
||||||
|
|||||||
+23
-2
@@ -10,6 +10,8 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use crate::config_store::{Account, AccountSyncState, Store};
|
use crate::config_store::{Account, AccountSyncState, Store};
|
||||||
|
|
||||||
|
const DEFAULT_API_BASE_URL: &str = "https://api.openai.com/v1";
|
||||||
|
|
||||||
#[derive(Debug, Serialize)]
|
#[derive(Debug, Serialize)]
|
||||||
struct LoginRequest<'a> {
|
struct LoginRequest<'a> {
|
||||||
username: &'a str,
|
username: &'a str,
|
||||||
@@ -163,9 +165,9 @@ pub async fn remote(json: bool) -> Result<()> {
|
|||||||
println!(
|
println!(
|
||||||
"{:<22} {:<34} {:<10} {:<12} {}",
|
"{:<22} {:<34} {:<10} {:<12} {}",
|
||||||
shorten(&account.id, 22),
|
shorten(&account.id, 22),
|
||||||
shorten(&account.email, 34),
|
shorten(account_email_display(account), 34),
|
||||||
account_auth_mode_name(account),
|
account_auth_mode_name(account),
|
||||||
account.plan_type.as_deref().unwrap_or("-"),
|
account_plan_display(account),
|
||||||
quota
|
quota
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -287,6 +289,25 @@ fn account_auth_mode_name(account: &Account) -> &'static str {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn account_email_display(account: &Account) -> &str {
|
||||||
|
if account.auth_mode == crate::config_store::AuthMode::ApiKey {
|
||||||
|
account
|
||||||
|
.api_base_url
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or(DEFAULT_API_BASE_URL)
|
||||||
|
} else {
|
||||||
|
&account.email
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn account_plan_display(account: &Account) -> &str {
|
||||||
|
if account.auth_mode == crate::config_store::AuthMode::ApiKey {
|
||||||
|
"-"
|
||||||
|
} else {
|
||||||
|
account.plan_type.as_deref().unwrap_or("-")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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