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
6 changes: 6 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@
- Honest desktop transcription-engine availability
- Automatic Faster Whisper model guidance

## v0.1.3

- ChatGPT/Codex account provider through the official local Codex CLI
- Subscription-login verification that refuses accidental API-key billing
- Sandboxed, ephemeral Codex transcript analysis with Luna as the default

## v0.2.0

- Better AI clipping workflow
Expand Down
5 changes: 5 additions & 0 deletions backend/routers/ai.py
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ async def nine_router_models(req: ModelListRequest):

@router.post("/ai/provider-check")
async def provider_check(req: ProviderCheckRequest):
if req.provider == "codex":
return AIProvider.check_codex(
executable_path=req.base_url,
model=req.model,
)
return AIProvider.check_cloud_provider(
provider=req.provider,
api_key=req.api_key,
Expand Down
129 changes: 129 additions & 0 deletions backend/scripts/smoke_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -798,6 +798,31 @@ def test_topic_edit_plan_never_deletes_everything_when_no_match_is_found(self) -
self.assertEqual(result["selectedSegments"], [])
self.assertEqual(result["suggestions"], [])

def test_codex_topic_edit_uses_larger_windows_to_reduce_plan_turns(self) -> None:
words = [
{
"index": index,
"word": f"word-{index}",
"start": index * 0.25,
"end": (index + 1) * 0.25,
}
for index in range(2400)
]

with patch.object(
ai_provider.AIProvider,
"complete",
return_value='{"relevantRanges": []}',
) as complete:
result = ai_provider.create_topic_edit_plan(
instruction="find one topic",
words=words,
provider="codex",
)

self.assertEqual(complete.call_count, 1)
self.assertEqual(result["metrics"]["chunkCount"], 1)

def test_xai_provider_uses_official_openai_compatible_endpoint(self) -> None:
with patch.object(
ai_provider,
Expand All @@ -817,6 +842,110 @@ def test_xai_provider_uses_official_openai_compatible_endpoint(self) -> None:
self.assertEqual(args[6], "xAI")
self.assertEqual(args[7], "xai")

def test_codex_status_requires_chatgpt_managed_login(self) -> None:
version = subprocess.CompletedProcess(
["codex", "--version"],
0,
stdout="codex-cli 1.2.3\n",
stderr="",
)
login = subprocess.CompletedProcess(
["codex", "login", "status"],
0,
stdout="Logged in using ChatGPT\n",
stderr="",
)
with (
patch.object(ai_provider, "_find_codex_executable", return_value="/tmp/codex"),
patch.object(ai_provider, "_run_codex_process", side_effect=[version, login]) as run,
):
result = ai_provider.AIProvider.check_codex(model="gpt-5.6-luna")

self.assertTrue(result["ok"])
self.assertTrue(result["authenticated"])
self.assertEqual(result["provider"], "codex")
self.assertIn("ChatGPT", result["message"])
self.assertIn("gpt-5.6-luna", result["models"])
self.assertEqual(run.call_count, 2)

def test_codex_status_rejects_api_key_login_to_avoid_separate_billing(self) -> None:
version = subprocess.CompletedProcess(
["codex", "--version"],
0,
stdout="codex-cli 1.2.3\n",
stderr="",
)
login = subprocess.CompletedProcess(
["codex", "login", "status"],
0,
stdout="Logged in using API key\n",
stderr="",
)
with (
patch.object(ai_provider, "_find_codex_executable", return_value="/tmp/codex"),
patch.object(ai_provider, "_run_codex_process", side_effect=[version, login]),
):
result = ai_provider.AIProvider.check_codex()

self.assertFalse(result["ok"])
self.assertTrue(result["authenticated"])
self.assertEqual(result["code"], "codex_api_key_login")
self.assertIn("included ChatGPT plan", result["message"])

def test_codex_provider_uses_ephemeral_read_only_cli_run(self) -> None:
completed = subprocess.CompletedProcess(
["codex", "exec"],
0,
stdout='{"ok": true}\n',
stderr="",
)
with (
patch.object(ai_provider, "_find_codex_executable", return_value="/tmp/codex"),
patch.object(
ai_provider.AIProvider,
"check_codex",
return_value={"ok": True, "message": "ready"},
),
patch.object(ai_provider, "_run_codex_process", return_value=completed) as run,
):
result = ai_provider.AIProvider.complete(
prompt='Return {"ok": true}',
provider="codex",
model="gpt-5.6-luna",
system_prompt="Return JSON only.",
)

self.assertEqual(result, '{"ok": true}')
args = run.call_args.args[1]
self.assertIn("exec", args)
self.assertIn("--ephemeral", args)
self.assertIn("--ignore-user-config", args)
self.assertIn("--ignore-rules", args)
self.assertIn("read-only", args)
self.assertIn("gpt-5.6-luna", args)
self.assertIn("<scriptcut_task>", run.call_args.kwargs["input_text"])

def test_codex_process_removes_ambient_openai_api_keys(self) -> None:
completed = subprocess.CompletedProcess(["codex"], 0, stdout="ok", stderr="")
with (
patch.dict(
ai_provider.os.environ,
{
"OPENAI_API_KEY": "should-not-leak",
"CODEX_API_KEY": "should-not-leak",
"SCRIPTCUT_SAFE_TEST": "kept",
},
clear=False,
),
patch.object(ai_provider.subprocess, "run", return_value=completed) as run,
):
ai_provider._run_codex_process("/tmp/codex", ["--version"])

environment = run.call_args.kwargs["env"]
self.assertNotIn("OPENAI_API_KEY", environment)
self.assertNotIn("CODEX_API_KEY", environment)
self.assertEqual(environment["SCRIPTCUT_SAFE_TEST"], "kept")

def test_cloud_provider_check_verifies_key_and_selected_model_without_completion(self) -> None:
response = SimpleNamespace(
ok=True,
Expand Down
Loading