Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions code_puppy/plugins/chatgpt_oauth/IMAGEGEN_SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ name: codex-imagegen
description: Generate raster images with gpt-image-2 through Codex OAuth. Activate when the user asks to create a photo, illustration, icon, sprite, texture, product image, banner, or other raster artwork.
version: "1.0"
author: code-puppy
tags:
- image-generation
- codex
- gpt-image
- artwork
---

# Codex Image Generation
Expand Down
16 changes: 16 additions & 0 deletions code_puppy/plugins/chatgpt_oauth/register_callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from code_puppy.i18n import t
from code_puppy.messaging import emit_error, emit_info, emit_success, emit_warning
from code_puppy.model_switching import set_model_and_reload_agent
from code_puppy.plugins.agent_skills.discovery import refresh_skill_cache

from .config import CHATGPT_OAUTH_CONFIG, get_token_storage_path
from .oauth_flow import run_oauth_flow
Expand Down Expand Up @@ -89,6 +90,10 @@ def _handle_chatgpt_logout() -> None:

if was_authenticated:
_reload_active_agent()
# Mirror the tool-unbinding reload above: the codex-imagegen skill is
# gated on auth too, so drop it from the skill cache immediately
# instead of leaving it visible until the next restart.
refresh_skill_cache()


def _is_codex_oauth_authenticated() -> bool:
Expand Down Expand Up @@ -125,6 +130,10 @@ def _handle_custom_command(command: str, name: str) -> Optional[bool]:
if name in {"chatgpt-auth", "codex-auth"}:
run_oauth_flow()
set_model_and_reload_agent("codex-gpt-5.6-sol")
# Authentication may have just succeeded, so the codex-imagegen skill
# (gated the same way as the codex_imagegen tool) needs to be
# re-discovered rather than waiting for the next process restart.
refresh_skill_cache()
return True

if name in {"chatgpt-status", "codex-status"}:
Expand Down Expand Up @@ -228,6 +237,13 @@ def _create_chatgpt_oauth_model(


def _register_imagegen_skill() -> list[dict[str, str]]:
# Gated the same way as _advertise_imagegen_tool below: the skill's own
# instructions tell the model to call codex_imagegen(...), which doesn't
# exist as an available tool for an unauthenticated user. Hiding the
# skill itself avoids the confusing case where a model activates it,
# tries the tool call, and only then discovers auth is missing.
if not _is_codex_oauth_authenticated():
return []
return [
{
"name": "codex-imagegen",
Expand Down
54 changes: 51 additions & 3 deletions code_puppy/plugins/chatgpt_oauth/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,14 +216,14 @@ def test_codex_imagegen_command():
def test_imagegen_skill_and_tool_registration():
from code_puppy.plugins.chatgpt_oauth import register_callbacks

skills = register_callbacks._register_imagegen_skill()
assert skills[0]["name"] == "codex-imagegen"
assert Path(skills[0]["skill_md_path"]).is_file()
with patch.object(
register_callbacks,
"load_stored_tokens",
return_value={"access_token": "token", "account_id": "account"},
):
skills = register_callbacks._register_imagegen_skill()
assert skills[0]["name"] == "codex-imagegen"
assert Path(skills[0]["skill_md_path"]).is_file()
assert register_callbacks._advertise_imagegen_tool("code-puppy") == [
"codex_imagegen"
]
Expand All @@ -233,6 +233,13 @@ def test_imagegen_skill_and_tool_registration():
]


def test_imagegen_skill_is_not_registered_when_logged_out():
from code_puppy.plugins.chatgpt_oauth import register_callbacks

with patch.object(register_callbacks, "load_stored_tokens", return_value=None):
assert register_callbacks._register_imagegen_skill() == []


def test_imagegen_tool_is_not_advertised_when_logged_out():
from code_puppy.plugins.chatgpt_oauth import register_callbacks

Expand All @@ -258,11 +265,52 @@ def test_logout_reloads_agent_to_unbind_imagegen(tmp_path):
patch.object(register_callbacks, "emit_info"),
patch.object(register_callbacks, "emit_success"),
patch.object(register_callbacks, "_reload_active_agent") as reload_agent,
patch.object(register_callbacks, "refresh_skill_cache") as refresh_skills,
):
register_callbacks._handle_chatgpt_logout()

assert not token_path.exists()
reload_agent.assert_called_once_with()
refresh_skills.assert_called_once_with()


def test_logout_does_not_refresh_skills_when_already_logged_out(tmp_path):
from code_puppy.plugins.chatgpt_oauth import register_callbacks

token_path = tmp_path / "tokens.json"
with (
patch.object(register_callbacks, "load_stored_tokens", return_value=None),
patch.object(
register_callbacks, "get_token_storage_path", return_value=token_path
),
patch.object(register_callbacks, "remove_chatgpt_models", return_value=0),
patch.object(register_callbacks, "emit_info"),
patch.object(register_callbacks, "emit_success"),
patch.object(register_callbacks, "_reload_active_agent") as reload_agent,
patch.object(register_callbacks, "refresh_skill_cache") as refresh_skills,
):
register_callbacks._handle_chatgpt_logout()

reload_agent.assert_not_called()
refresh_skills.assert_not_called()


def test_auth_command_refreshes_skill_cache():
from code_puppy.plugins.chatgpt_oauth import register_callbacks

with (
patch.object(register_callbacks, "run_oauth_flow") as oauth_flow,
patch.object(register_callbacks, "set_model_and_reload_agent") as set_model,
patch.object(register_callbacks, "refresh_skill_cache") as refresh_skills,
):
assert (
register_callbacks._handle_custom_command("/codex-auth", "codex-auth")
is True
)

oauth_flow.assert_called_once_with()
set_model.assert_called_once_with("codex-gpt-5.6-sol")
refresh_skills.assert_called_once_with()


def test_codex_imagegen_agent_tool(tmp_path):
Expand Down
Loading