Files
codex/prs/bolinfest/study/PR-1971-study.md
2025-09-02 15:17:45 -07:00

150 lines
5.7 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
**DOs**
- Bold the logic: Prefer ChatGPT auth for Free/Plus/Pro/Team plans even if an API key is present.
- Bold the fallback: Read `OPENAI_API_KEY` only when `auth.json` is missing (`NotFound`) and `include_env_var` is true.
- Bold the error handling: If `auth.json` exists but is malformed, return the error and do not fall back to env.
- Bold the API key choice: Use API key when `auth.json` has an API key and no tokens.
- Bold the enterprise routing: Use API key for Business/Enterprise/Edu or unknown plans.
- Bold the modeling: Represent plan with `PlanType` (`Known(KnownPlan)` or `Unknown(String)`) and use `serde` for `KnownPlan`.
- Bold the helper: Add `read_openai_api_key_from_env()` to encapsulate env access.
- Bold the ChatGPT shape: In ChatGPT mode, set `api_key: None` and keep only tokens/last_refresh; clear `openai_api_key`.
- Bold the display: Expose `IdTokenInfo::get_chatgpt_plan_type()` for UI and title-case it for display.
- Bold the tests: Use a minimal URL-safe, no-pad base64 JWT, a `LAST_REFRESH` constant, and `tempdir` helpers.
```rust
// Auth selection (core decision points)
if let Ok(auth) = try_read_auth_json(&auth_file) {
let AuthDotJson { openai_api_key, tokens, last_refresh } = auth;
if let Some(api_key) = &openai_api_key {
match &tokens {
Some(tokens) if tokens.is_plan_that_should_use_api_key() => {
// Business/Enterprise/Edu/Unknown → API key
return Ok(Some(CodexAuth::from_api_key(api_key)));
}
Some(_) => {
// Free/Plus/Pro/Team → prefer ChatGPT
}
None => {
// API key but no tokens → API key
return Ok(Some(CodexAuth::from_api_key(api_key)));
}
}
}
// ChatGPT mode: no API key, keep tokens/last_refresh
return Ok(Some(CodexAuth {
api_key: None,
mode: AuthMode::ChatGPT,
auth_file,
auth_dot_json: Arc::new(Mutex::new(Some(AuthDotJson {
openai_api_key: None,
tokens,
last_refresh,
}))),
}));
} else if include_env_var && matches!(e.kind(), std::io::ErrorKind::NotFound) {
// Only when auth.json is missing
if let Some(k) = read_openai_api_key_from_env() {
return Ok(Some(CodexAuth::from_api_key(&k)));
}
return Ok(None);
} else {
// Malformed auth.json → surface error
return Err(e);
}
```
```rust
// Plan modeling
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
enum PlanType {
Known(KnownPlan),
Unknown(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
enum KnownPlan { Free, Plus, Pro, Team, Business, Enterprise, Edu }
impl PlanType {
fn is_plan_that_should_use_api_key(&self) -> bool {
match self {
PlanType::Known(KnownPlan::Free | KnownPlan::Plus | KnownPlan::Pro | KnownPlan::Team) => false,
PlanType::Known(_) | PlanType::Unknown(_) => true,
}
}
}
impl TokenData {
pub(crate) fn is_plan_that_should_use_api_key(&self) -> bool {
self.id_token.chatgpt_plan_type.as_ref().is_none_or(|p| p.is_plan_that_should_use_api_key())
}
}
```
```rust
// Env helper
fn read_openai_api_key_from_env() -> Option<String> {
env::var(OPENAI_API_KEY_ENV_VAR).ok().filter(|s| !s.is_empty())
}
```
```rust
// UI display
let plan_text = info
.get_chatgpt_plan_type()
.map(|s| title_case(&s))
.unwrap_or_else(|| "Unknown".to_string());
lines.push(Line::from(vec![" • Plan: ".into(), plan_text.into()]));
if let Some(email) = &info.email {
lines.push(Line::from(vec![" • Login: ".into(), email.clone().into()]));
}
```
```rust
// Test helper: write a minimal auth.json
let header_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
serde_json::to_vec(&json!({ "alg": "none", "typ": "JWT" }))?,
);
let payload_b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(
serde_json::to_vec(&json!({
"email": "user@example.com",
"https://api.openai.com/auth": {
"chatgpt_plan_type": "pro",
"user_id": "user-12345"
}
}))?,
);
let fake_jwt = format!("{header_b64}.{payload_b64}.{}",
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"sig"));
```
**DONTs**
- Bold the wrong fallback: Do not fall back to `OPENAI_API_KEY` when `auth.json` exists but is malformed.
- Bold the plan mistake: Do not use API key for Free/Plus/Pro/Team when tokens indicate those plans.
- Bold the token ignore: Do not ignore tokens/plan when choosing between ChatGPT and API key.
- Bold the leakage: Do not carry `openai_api_key` forward in `auth_dot_json` when using ChatGPT mode; set it to `None`.
- Bold the ambiguity: Do not treat unknown plan strings as ChatGPT-eligible; unknown plans use API key.
- Bold the env misuse: Do not read `OPENAI_API_KEY` if `include_env_var` is false or `auth.json` is present.
- Bold the ownership slip: Do not move `email` out of `IdTokenInfo`; borrow (`&info.email`) and clone only when needed for ownership.
- Bold the string handling: Do not hand-roll plan strings; use `PlanType`/`KnownPlan` plus `get_chatgpt_plan_type()` and `title_case`.
```rust
// WRONG: falling back to env on malformed auth.json
match try_read_auth_json(&auth_file) {
Err(_) if include_env_var => read_openai_api_key_from_env().map(...), // ✗ Dont do this
_ => {}
}
// WRONG: treating unknown plan as ChatGPT
match plan_type {
PlanType::Unknown(_) => use_chatgpt(), // ✗ Should use API key
_ => {}
}
// WRONG: keeping API key around in ChatGPT mode
CodexAuth { api_key: Some(api_key), mode: AuthMode::ChatGPT, .. } // ✗ api_key must be None
```