api: support local Codex CLI and Claude#95
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4c1e6ab8b6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if eventType == "response.output_text.delta", | ||
| let textChunk = eventPayload["delta"] as? String { | ||
| accumulatedResponseText += textChunk | ||
| let currentAccumulatedText = accumulatedResponseText | ||
| await onTextChunk(currentAccumulatedText) | ||
| } else if eventType == "response.output_text.done", | ||
| accumulatedResponseText.isEmpty, | ||
| let completedText = eventPayload["text"] as? String { | ||
| accumulatedResponseText = completedText | ||
| await onTextChunk(completedText) | ||
| } else if eventType == "error" { |
There was a problem hiding this comment.
Parse refusal events from the Codex SSE stream
The streaming parser only consumes response.output_text.* events and ignores response.refusal.*, so safety refusals from the Responses API produce an empty accumulatedResponseText instead of a spoken/user-visible explanation. In practice this makes blocked prompts look like silent failures (no TTS text), which is a regression in runtime behavior for moderated inputs.
Useful? React with 👍 / 👎.
| } else if eventType == "error" { | ||
| let message = Self.extractErrorMessage(from: eventPayload) | ||
| throw NSError( | ||
| domain: "CodexAPI", | ||
| code: -1, | ||
| userInfo: [NSLocalizedDescriptionKey: message] | ||
| ) | ||
| } |
There was a problem hiding this comment.
Treat response.failed SSE events as errors
The Responses API can send a response.failed lifecycle event on a 200 streaming connection, but this parser only throws on event type error and otherwise falls through. When that happens, the method returns an empty string as if it succeeded, and the caller skips both TTS and fallback error speech, creating a silent no-response path for users.
Useful? React with 👍 / 👎.
| private static func persistedAIProvider() -> CompanionAIProvider { | ||
| guard let storedProvider = UserDefaults.standard.string(forKey: "selectedAIProvider"), | ||
| let provider = CompanionAIProvider(rawValue: storedProvider) else { | ||
| return defaultAIProvider | ||
| } |
There was a problem hiding this comment.
Preserve Claude preference when selectedAIProvider is unset
On upgrade, users coming from the previous Claude-only flow have no selectedAIProvider key, so this fallback forces .codex and ignores their existing Claude setup. In environments where only ANTHROPIC_API_KEY is configured (the prior default), first-run requests now fail against /chat/codex until the user manually changes providers, which is a user-facing regression introduced by the new default.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9154d36282
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| process.waitUntilExit() | ||
|
|
||
| let standardOutput = String(data: outputPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" | ||
| let standardError = String(data: errorPipe.fileHandleForReading.readDataToEndOfFile(), encoding: .utf8) ?? "" |
There was a problem hiding this comment.
Drain subprocess output before waiting for exit
This waits for codex to exit before reading either pipe, which can deadlock if the CLI writes enough stdout/stderr to fill the OS pipe buffer (common with verbose/error output). In that case the child blocks on write, never exits, and the app can stay stuck in the processing state indefinitely.
Useful? React with 👍 / 👎.
| userPrompt: String | ||
| ) async throws -> String { | ||
| let selectedModel = model | ||
| return try await Task.detached(priority: .userInitiated) { |
There was a problem hiding this comment.
Cancel detached Codex runs when interaction is canceled
The CLI execution is launched in an unstructured Task.detached, so canceling the parent response task (for example when the user starts a new push-to-talk turn) doesn't stop the in-flight Codex process. This can leave orphaned codex exec runs consuming CPU/network in the background and piling up across rapid interruptions.
Useful? React with 👍 / 👎.
| let candidatePaths = [ | ||
| "/opt/homebrew/bin/codex", | ||
| "/usr/local/bin/codex", | ||
| "/usr/bin/codex" | ||
| ] | ||
|
|
||
| for candidatePath in candidatePaths where FileManager.default.isExecutableFile(atPath: candidatePath) { |
There was a problem hiding this comment.
Resolve Codex binary from PATH instead of fixed paths
Executable discovery only checks three hard-coded locations, so installs in common custom prefixes (for example npm/nvm global bins) are treated as missing even when codex is correctly on the user's PATH. That makes the Codex provider fail to start on otherwise valid setups.
Useful? React with 👍 / 👎.
| "--sandbox", "read-only", | ||
| "--cd", temporaryDirectory.path, |
There was a problem hiding this comment.
Constrain Codex sandbox to prevent arbitrary file reads
Running codex exec with --sandbox read-only still allows shell execution and read access to files outside the temp screenshot directory, so prompt injection from on-screen content can coerce the model to read sensitive local files (for example ~/.ssh or browser data) and echo them back in the assistant response. This is a new privacy/security exposure compared with the previous server-only provider path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3ad2d5a53c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| private lazy var claudeAPI: ClaudeAPI = { | ||
| return ClaudeAPI(proxyURL: "\(Self.workerBaseURL)/chat", model: selectedModel) | ||
| return ClaudeAPI(proxyURL: "\(Self.workerBaseURL)/chat/claude", model: selectedModel) |
There was a problem hiding this comment.
Keep Claude API path backward-compatible
Pointing ClaudeAPI at /chat/claude makes the app depend on a newly added Worker route, so deployments where the app updates before the Worker redeploy (older Worker only exposing /chat) will get 404s for every Claude request. Using the existing /chat path from the app side would keep Claude functional across staggered rollouts while still working with the updated Worker.
Useful? React with 👍 / 👎.
Summary
codex exec, attached screenshots, and the selected Codex model. No OpenAI API key or Worker Codex endpoint is required.gpt-5.5,gpt-5.4,gpt-5.4-mini,gpt-5.3-codex,gpt-5.3-codex-spark, andgpt-5.2, withgpt-5.5as the default.codex loginfor Codex andANTHROPIC_API_KEYonly for Claude chat.Validation
swiftc -parse leanring-buddy/*.swiftnpx --yes --package typescript tsc --noEmit --target ES2022 --module ESNext worker/src/index.tsgit diff --checkcodex exec --helpchecked for supported CLI flags