mpfaffenberger / mpfaffenberger/code_puppy
OAuth provider plugins (chatgpt_oauth / claude_code_oauth / copilot_auth + bedrock/foundry) re-implement PKCE, token refresh, and <provider>_models.json plumbing in parallel
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 814
- Forks
- 278
- Avg merge
- 2d 5h
- Merged PRs (30d)
- 76
Description
Theme
Three OAuth-style provider plugins each ship a 500-650 line utils.py that is the same machine with provider constants swapped in. (Distinct from #406, which covers credential lookup in core config.py/model_factory.py/provider_credentials.py — this is the plugin-tier auth/token/models-file stack.)
1. PKCE / OAuth context scaffolding (chatgpt vs claude_code)
plugins/chatgpt_oauth/utils.py:29-112 vs plugins/claude_code_oauth/utils.py:33-112:
class OAuthContextdataclass_urlsafe_b64encode,_generate_code_verifier,_compute_code_challenge(verifier differs only intoken_hex(64)vs_urlsafe_b64encode(token_bytes(64)))prepare_oauth_context,assign_redirect_uri,build_authorization_url— identical shape, onlyCHATGPT_OAUTH_CONFIGvsCLAUDE_CODE_OAUTH_CONFIGkeys and a couple of query params differ.
Drift already visible: claude_code caches the context in a module global (get_oauth_context/clear_oauth_context); chatgpt adds a 4-minute expires_at; claude_code validates nothing about ports while chatgpt enforces required_port. None of these differences are provider-mandated — they are divergent maintenance.
2. refresh/load/save token flows
chatgpt_oauth/utils.py:141-256(load_stored_tokens,get_valid_access_token,refresh_access_token,save_tokens)claude_code_oauth/utils.py:129-311(same five concepts + expiry-buffer helpers)copilot_auth/utils.py:287-382(_persist_session,_load_persisted_session,get_valid_session_token— same load-check-expiry-refresh-persist loop for device-flow tokens)
All three are: read JSON token file -> check expiry -> POST refresh -> merge -> write JSON -> return access token, each with its own logging strings and subtle differences in which fields survive the merge.
3. <provider>_models.json load/save/remove (5x)
The same three functions exist five times:
chatgpt_oauth/utils.py:258-279,514-530—load_chatgpt_models/save_chatgpt_models/remove_chatgpt_modelsclaude_code_oauth/utils.py:313-322,371-381,642-658—load_claude_models/save_claude_models/remove_claude_code_modelscopilot_auth/utils.py:408-451—load_copilot_models/save_copilot_models/remove_copilot_modelsaws_bedrock/utils.py:17-44,129—load_extra_models/save_extra_models/remove_bedrock_models_from_configazure_foundry/utils.py:102-135,318—load_extra_models/save_extra_models/remove_foundry_models_from_config
Each is try/open/json.load with a logged fallback, json.dump(indent=2), and a remove that filters by a provider marker key. Drift example: copilot validates isinstance(data, dict) and warns on non-object JSON; chatgpt and claude_code silently return whatever json.load gives them, so a corrupted chatgpt_models.json containing a list propagates into model config.
Proposed shared abstraction
Two small modules under code_puppy/plugins/_provider_common/ (builtin-tier shared lib, no hook changes needed):
# pkce.py
@dataclass
class PKCEContext: ...
def new_pkce_context(*, urlsafe: bool = True) -> PKCEContext: ...
def build_authorization_url(cfg: Mapping, ctx: PKCEContext, extra_params: Mapping = ()) -> str: ...
# token_store.py
class JsonTokenStore:
def __init__(self, path: Path): ...
def load(self) -> dict | None: ...
def save(self, tokens: dict) -> bool: ...
def refresh(self, *, token_url, client_id, merge_keys=("id_token",)) -> str | None: ...
# models_file.py
class ProviderModelsFile:
def __init__(self, path: Path, source_marker: str): ...
def load(self) -> dict: ... # with isinstance(dict) validation (copilot's behavior)
def save(self, models: dict) -> bool: ...
def remove_all(self) -> int: ... # filter by source_marker
What stays per-plugin: provider config dicts, model-entry builders (_build_model_entry, settings inference), model filtering (filter_latest_claude_models), and device-flow specifics. The win: a fourth OAuth provider plugin becomes ~150 lines instead of ~600, and token-file corruption handling is fixed in one place.
Filed by Zen Reviewer C (code-puppy-60635a) — DRY review round
Contributor guide
No contributing guide indexed for this repository
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by comparing the named ranges in plugins/chatgpt_oauth/utils.py, plugins/claude_code_oauth/utils.py, copilot_auth/utils.py, aws_bedrock/utils.py, and azure_foundry/utils.py, then inspect the proposed code_puppy/plugins/_provider_common/ modules. Done means the shared PKCE, token-store, and models-file behavior replaces duplicated plugin logic while provider-specific builders and device-flow details remain intact; no tests are named in the issue.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- authentication, tooling
- Issue type
- Refactor
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100