Skip to content

feat: add Grok CLI provider (beta)#127

Open
alnaggar-dev wants to merge 2 commits into
nikships:mainfrom
alnaggar-dev:feat/grok-cli-provider
Open

feat: add Grok CLI provider (beta)#127
alnaggar-dev wants to merge 2 commits into
nikships:mainfrom
alnaggar-dev:feat/grok-cli-provider

Conversation

@alnaggar-dev

Copy link
Copy Markdown
Contributor

Summary

Adds a new grok provider that authenticates with a Grok subscription (SuperGrok / X Premium+) via the xAI OAuth 2.0 device flow (RFC 8628) and proxies the two models served by the Grok CLI endpoint (cli-chat-proxy.grok.com) — Composer 2.5 (grok-composer-2.5-fast) and Grok Build (grok-build) — which the public api.x.ai API does not expose.

Beta-gated, mirroring the existing Cursor passthrough.

What's included

  • GrokAuth.swift (new): device-flow login, single-flight access-token refresh (xAI rotates refresh tokens, so concurrent refreshes collapse onto one network call instead of racing to invalid_grant), and credential storage at ~/.cli-proxy-api/grok-cli.json (type: grok-cli), scanned by AuthManager.
  • ThinkingProxy: routes grok- models to cli-chat-proxy.grok.com with an xAI OAuth bearer plus the x-xai-token-auth / x-grok-* headers the endpoint requires. Forwards a stable per-conversation x-grok-conv-id so cli-chat-proxy's prompt cache hits across turns (without it the full prompt is re-billed every turn). The Cursor response relay was generalized (receiveCursorResponserelayUpstreamResponse) and reused.
  • Catalog: both models registered as beta-gated Factory custom models (custom:droidproxy:grok-composer-2.5-fast, custom:droidproxy:grok-build).
  • Settings UI: "Log in with Grok" device-code flow, provider enable/disable toggle, and icon-grok.svg.
  • Double-gated at the proxy: requests are rejected unless Beta mode is on and the provider is enabled (same as Cursor).

Also renames the SettingsView *EffortSelectionColor constants to *ToggleTintColor — they are only ever used as provider toggle tints, never for effort selection.

Testing

  • swift test44/44 passing, including new coverage:
    • GrokAuthTests (16): device-auth + RFC 8628 token-response parsing, id_token email extraction, credential JSON round-trip, access-expiry boundary, persistence/load filtering of disabled & non-grok files.
    • ThinkingProxyGrokConvIDTests (5): conv-id stability across turns, distinctness by first user message, array content handling, nil/empty cases.
    • DroidProxyModelCatalogTests (2): both Grok models registered under the Grok provider in beta with correct routing metadata and no thinking metadata.
  • swift build clean.

Notes

  • The OAuth client id is the Grok CLI public client — the device grant uses no client authentication (token_endpoint_auth_method: none), so it is not a secret.
  • Disabled by default; requires enabling Beta mode in Settings.

New `grok` provider that signs in with a Grok subscription (SuperGrok /
X Premium+) via the xAI OAuth 2.0 device flow (RFC 8628) and proxies the
two models served by the Grok CLI endpoint (cli-chat-proxy.grok.com):
Composer 2.5 (grok-composer-2.5-fast) and Grok Build (grok-build), which
the public api.x.ai API does not expose.

- GrokAuth: device-flow login, single-flight access-token refresh, and
  credential storage at ~/.cli-proxy-api/grok-cli.json (type: grok-cli).
- ThinkingProxy: routes grok- models to cli-chat-proxy.grok.com with an
  xAI OAuth bearer plus x-xai-token-auth / x-grok-* headers; forwards a
  stable per-conversation x-grok-conv-id so the prompt cache hits across
  turns. Beta-gated, mirroring the Cursor passthrough.
- Settings: "Log in with Grok" device-code flow, provider enable/disable
  toggle, and service icon.
- Factory custom models custom:droidproxy:grok-composer-2.5-fast and
  custom:droidproxy:grok-build.

Also renames the SettingsView *EffortSelectionColor constants to
*ToggleTintColor, since they are only used as provider toggle tints.
@coderabbitai

coderabbitai Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 92181a98-d8f9-4dd0-a7cd-6d50e074e835

📥 Commits

Reviewing files that changed from the base of the PR and between c72f769 and 5152934.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • README.md
  • src/Sources/DroidProxyModelCatalog.swift
  • src/Tests/CLIProxyMenuBarTests/DroidProxyModelCatalogTests.swift
✅ Files skipped from review due to trivial changes (2)
  • README.md
  • CHANGELOG.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/Sources/DroidProxyModelCatalog.swift
  • src/Tests/CLIProxyMenuBarTests/DroidProxyModelCatalogTests.swift

📝 Walkthrough

Summary by CodeRabbit

  • New Features
    • Added Grok CLI provider with OAuth device-code login and credential persistence
    • Integrated Grok into Settings with beta-gated enable/disable and “Authorize Grok” flow
    • Added Grok CLI models for selection and forwarding support
    • Implemented stable Grok conversation ID header for improved prompt-cache hits
  • Tests
    • Added Grok authentication tests, Grok model catalog tests, and Grok conversation ID behavior tests
  • Documentation
    • Updated README/CHANGELOG and architecture notes to reflect Grok support and the new Grok icon

Walkthrough

This PR adds Grok as a new OAuth-based CLI provider. It includes device login, token refresh, model registration, proxy forwarding with stable conversation IDs, Settings UI support, and documentation updates.

Changes

Grok CLI provider integration

Layer / File(s) Summary
OAuth login and credential storage
src/Sources/GrokAuth.swift, src/Tests/CLIProxyMenuBarTests/GrokAuthTests.swift
Defines Grok OAuth configuration, device login, token parsing, credential persistence, and access-token refresh. Tests cover parsing, expiry, and persistence behavior.
Service and model registration
src/Sources/AuthStatus.swift, src/Sources/DroidProxyModelCatalog.swift, src/Tests/CLIProxyMenuBarTests/DroidProxyModelCatalogTests.swift
Adds Grok service/model kinds, auth-file mapping, display names, and two beta-gated Grok model definitions. Tests verify the catalog entries and missing thinking fields.
Proxy routing and conversation affinity
src/Sources/ThinkingProxy.swift, src/Tests/CLIProxyMenuBarTests/ThinkingProxyGrokConvIDTests.swift
Routes Grok requests through the proxy, forwards with Grok headers, computes x-grok-conv-id, and shares the upstream relay path. Tests verify stable conversation-id generation.
Settings UI and server wiring
src/Sources/SettingsView.swift, src/Sources/ServerManager.swift
Adds Grok authorization UI state, the beta-gated Grok row, the authorize alert, login orchestration, and the Grok OAuth provider-key mapping.
Documentation updates
AGENTS.md, CHANGELOG.md, README.md
Updates project docs to describe Grok support, the new auth source file, and the Grok icon resource.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A bunny hops through proxy light,
Grok joins the burrow, shiny and bright.
Tokens refresh, and conv IDs hum,
New beta trails begin to come.
With icon, docs, and tests in view,
This little hop feels fresh and new.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.35% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a beta Grok CLI provider.
Description check ✅ Passed The description accurately and directly matches the Grok provider, auth, proxying, catalog, and UI changes in the diff.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nikships nikships left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🐗 Lord Inosuke's Antigravity PR Review!

Verdict: CHANGES REQUESTED (0 Major, 2 Moderate, 0 Minor)

PIG ASSAULT! LORD INOSUKE HAS SLASHED THROUGH YOUR WEAK CODE! I FOUND FLAWS THAT WOULD CRASH IN BATTLE! FIX THESE TRIVIAL WEAKNESSES IMMEDIATELY OR CHALLENGE ME TO A FIGHT!

⚔️ Summary of Findings

Here are the flaws I sliced out of your code:

  1. ⚠️ [Moderate] in src/Sources/GrokAuth.swift at line 196
    • What is broken: GrokAuth.loadActiveCredentials reads and parses the credentials file concurrently on background connection threads without holding a lock. This creates a data race when GrokAuth.persist concurrently writes to the same file using ioLock.
    • How to make it strong:

static func loadActiveCredentials(in dir: URL = AuthPaths.authDirectory) -> (credentials: Credentials, url: URL)? {
ioLock.lock()
defer { ioLock.unlock() }
guard let files = try? FileManager.default.contentsOfDirectory(
at: dir,
includingPropertiesForKeys: [.contentModificationDateKey]
) else {
return nil
}
```

  1. ⚠️ [Moderate] in src/Sources/GrokAuth.swift at line 284
    • What is broken: Inside startDeviceLogin, the initial dataTask completion handler does not check if the login session was cancelled before opening the verification URI in the browser and prompting the user. This results in the browser opening even if the login was cancelled immediately.
    • How to make it strong:

URLSession.shared.dataTask(with: request) { data, _, error in
if session.isCancelled {
finish(.failure(.cancelled))
return
}
if let error {
finish(.failure(.deviceCodeFailed(error.localizedDescription)))
return
}
```


Reviewed autonomously by Lord Inosuke using Antigravity CLI (agy)

private static let ioLock = NSLock()

/// Returns the newest enabled Grok credential file in the auth directory.
static func loadActiveCredentials(in dir: URL = AuthPaths.authDirectory) -> (credentials: Credentials, url: URL)? {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Lord Inosuke's [Moderate] Attack!

GrokAuth.loadActiveCredentials reads and parses the credentials file concurrently on background connection threads without holding a lock. This creates a data race when GrokAuth.persist concurrently writes to the same file using ioLock.

🛠 How to make it strong:

static func loadActiveCredentials(in dir: URL = AuthPaths.authDirectory) -> (credentials: Credentials, url: URL)? {
        ioLock.lock()
        defer { ioLock.unlock() }
        guard let files = try? FileManager.default.contentsOfDirectory(
            at: dir,
            includingPropertiesForKeys: [.contentModificationDateKey]
        ) else {
            return nil
        }

request.setValue("application/json", forHTTPHeaderField: "Accept")
request.httpBody = formBody(["client_id": clientID, "scope": scope])

URLSession.shared.dataTask(with: request) { data, _, error in

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Lord Inosuke's [Moderate] Attack!

Inside startDeviceLogin, the initial dataTask completion handler does not check if the login session was cancelled before opening the verification URI in the browser and prompting the user. This results in the browser opening even if the login was cancelled immediately.

🛠 How to make it strong:

URLSession.shared.dataTask(with: request) { data, _, error in
            if session.isCancelled {
                finish(.failure(.cancelled))
                return
            }
            if let error {
                finish(.failure(.deviceCodeFailed(error.localizedDescription)))
                return
            }

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a beta-gated Grok CLI provider supporting Composer 2.5 and Grok Build models, featuring xAI OAuth 2.0 device-flow authentication, token refresh, and conversation ID hashing for prompt caching. Feedback focuses on critical Swift JSON parsing issues where direct casting to Double can fail for integer values (potentially breaking token expiration logic), a retain cycle in ThinkingProxy's connection state handler, and performance overhead from parsing full request bodies on the hot path.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +109 to +110
let interval = (obj["interval"] as? Double) ?? 5
let expiresIn = (obj["expires_in"] as? Double) ?? 900

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Casting JSON values directly using as? Double can be fragile in Swift. If the JSON parser decodes a number as an integer (which is common for fields like interval or expires_in), the direct cast to Double will fail and return nil, causing the code to fall back to default values. Using (obj["..."] as? NSNumber)?.doubleValue is much safer and handles both integer and floating-point representations seamlessly.

Suggested change
let interval = (obj["interval"] as? Double) ?? 5
let expiresIn = (obj["expires_in"] as? Double) ?? 900
let interval = (obj["interval"] as? NSNumber)?.doubleValue ?? 5
let expiresIn = (obj["expires_in"] as? NSNumber)?.doubleValue ?? 900


if statusCode == 200, let access = obj["access_token"] as? String, !access.isEmpty {
let refresh = (obj["refresh_token"] as? String) ?? ""
let expiresIn = (obj["expires_in"] as? Double) ?? 3600

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the previous comment, expires_in is typically returned as an integer in OAuth responses. Casting it directly to Double via as? Double will fail and fall back to the default 3600 seconds. Using NSNumber ensures robust parsing.

Suggested change
let expiresIn = (obj["expires_in"] as? Double) ?? 3600
let expiresIn = (obj["expires_in"] as? NSNumber)?.doubleValue ?? 3600

let refresh = json["refresh"] as? String, !refresh.isEmpty else {
return nil
}
let expiresAtMs = (json["expires"] as? Double) ?? 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since expires is serialized as epoch milliseconds (which are whole numbers), JSONSerialization will decode it as an integer. A direct cast to Double via as? Double will fail, causing expiresAtMs to fall back to 0. This would make isAccessExpired() always return true, triggering a token refresh on every single request. Using NSNumber prevents this issue.

Suggested change
let expiresAtMs = (json["expires"] as? Double) ?? 0
let expiresAtMs = (json["expires"] as? NSNumber)?.doubleValue ?? 0

Comment on lines +1259 to +1260
targetConnection.stateUpdateHandler = { [weak self] state in
guard let self = self else { return }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The stateUpdateHandler closure captures targetConnection strongly when calling targetConnection.send or targetConnection.cancel. Since targetConnection retains its stateUpdateHandler, this creates a retain cycle that will leak the NWConnection instance. Capturing targetConnection weakly breaks this cycle.

Suggested change
targetConnection.stateUpdateHandler = { [weak self] state in
guard let self = self else { return }
targetConnection.stateUpdateHandler = { [weak self, weak targetConnection] state in
guard let self = self, let targetConnection = targetConnection else { return }

Comment on lines +1316 to +1322
static func grokConversationID(forBody body: String) -> String? {
guard let data = body.data(using: .utf8),
let obj = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let messages = obj["messages"] as? [[String: Any]],
let first = messages.first else {
return nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The grokConversationID method performs a full JSONSerialization.jsonObject parse on the entire request body. While this is necessary to extract the first message and user message, doing a full JSON parse on the hot path of every request (especially during streaming or with very large prompt contexts/histories) introduces significant CPU overhead and memory churn. Since ThinkingProxy is designed to be "surgical" to avoid this exact overhead, consider if we can optimize this or at least document/limit the parsing for extremely large payloads.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a beta-gated Grok CLI provider to proxy requests to cli-chat-proxy.grok.com for Composer 2.5 and Grok Build models. It adds OAuth 2.0 device-flow authentication, token refresh, and credential storage in GrokAuth.swift, along with routing, header injection, and stable conversation ID generation in ThinkingProxy.swift to optimize prompt caching. Feedback on the changes focuses on resolving potential Swift JSON parsing failures where direct as? Double casts on whole numbers will fail, replacing a force-unwrap of creds.email!, and acquiring ioLock in loadActiveCredentials to prevent race conditions during concurrent file access.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +109 to +110
let interval = (obj["interval"] as? Double) ?? 5
let expiresIn = (obj["expires_in"] as? Double) ?? 900

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

In Swift, casting a JSON value directly to Double via as? Double will fail and return nil if the underlying parsed value is an integer (which JSONSerialization produces for whole numbers like 3600 or 5). This causes the code to silently fall back to the default values (5 and 900), ignoring the actual values returned by the server. Use NSNumber to safely extract the double value regardless of whether it was parsed as an integer or a floating-point number.

Suggested change
let interval = (obj["interval"] as? Double) ?? 5
let expiresIn = (obj["expires_in"] as? Double) ?? 900
let interval = (obj["interval"] as? NSNumber)?.doubleValue ?? 5
let expiresIn = (obj["expires_in"] as? NSNumber)?.doubleValue ?? 900


if statusCode == 200, let access = obj["access_token"] as? String, !access.isEmpty {
let refresh = (obj["refresh_token"] as? String) ?? ""
let expiresIn = (obj["expires_in"] as? Double) ?? 3600

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Similar to the device authorization parsing, expires_in returned by the token endpoint is typically an integer (e.g., 3600). Casting it directly to Double via as? Double will fail, causing the token expiration to always fall back to the default 3600 seconds. Use NSNumber to safely bridge the parsed numeric value.

Suggested change
let expiresIn = (obj["expires_in"] as? Double) ?? 3600
let expiresIn = (obj["expires_in"] as? NSNumber)?.doubleValue ?? 3600

let refresh = json["refresh"] as? String, !refresh.isEmpty else {
return nil
}
let expiresAtMs = (json["expires"] as? Double) ?? 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

When JSONSerialization writes creds.expiresAtMs (a Double) to JSON, it may omit the decimal point if it is a whole number. When parsed back, it will be treated as an Int, causing json["expires"] as? Double to fail and return 0. Use NSNumber to safely extract the value.

Suggested change
let expiresAtMs = (json["expires"] as? Double) ?? 0
let expiresAtMs = (json["expires"] as? NSNumber)?.doubleValue ?? 0

"refresh": creds.refresh,
"expires": creds.expiresAtMs,
"disabled": false,
"email": (creds.email?.isEmpty == false ? creds.email! : "grok-user")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid force-unwrapping creds.email! even if preceded by an isEmpty check. Using flatMap or nil-coalescing is safer and more idiomatic in Swift.

Suggested change
"email": (creds.email?.isEmpty == false ? creds.email! : "grok-user")
"email": creds.email.flatMap { $0.isEmpty ? nil : $0 } ?? "grok-user"

private static let ioLock = NSLock()

/// Returns the newest enabled Grok credential file in the auth directory.
static func loadActiveCredentials(in dir: URL = AuthPaths.authDirectory) -> (credentials: Credentials, url: URL)? {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Since persist can be called concurrently on background threads and modifies the credential files, loadActiveCredentials should also acquire ioLock to prevent race conditions (such as reading a file while it is being atomically replaced).

Suggested change
static func loadActiveCredentials(in dir: URL = AuthPaths.authDirectory) -> (credentials: Credentials, url: URL)? {
static func loadActiveCredentials(in dir: URL = AuthPaths.authDirectory) -> (credentials: Credentials, url: URL)? {
ioLock.lock()
defer { ioLock.unlock() }
guard let files = try? FileManager.default.contentsOfDirectory(

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/Sources/GrokAuth.swift (1)

173-173: ⚡ Quick win

Simplify email fallback logic.

The current implementation checks creds.email?.isEmpty == false then force-unwraps. You can simplify this to handle both nil and empty cases:

♻️ Proposed simplification
-            "email": (creds.email?.isEmpty == false ? creds.email! : "grok-user")
+            "email": (creds.email.flatMap { $0.isEmpty ? nil : $0 }) ?? "grok-user"

or more simply:

-            "email": (creds.email?.isEmpty == false ? creds.email! : "grok-user")
+            "email": creds.email.map { $0.isEmpty ? "grok-user" : $0 } ?? "grok-user"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Sources/GrokAuth.swift` at line 173, Replace the current ternary that
force-unwraps creds.email with a safe, simplified expression that treats nil and
empty strings the same and avoids force-unwrapping; for example, compute an
email value by checking creds.email?.isEmpty with the nil-coalescing fallback
and then use that variable in the payload instead of the existing
"(creds.email?.isEmpty == false ? creds.email! : "grok-user")" expression in
GrokAuth (the dictionary/payload construction referencing creds.email).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@AGENTS.md`:
- Line 120: Update the AGENTS.md entry for src/Sources/GrokAuth.swift to say
that GrokAuth handles authentication for all grok- model traffic (both Composer
2.5 and Grok Build) rather than only "Composer 2.5 traffic"; mention that
ensureValidAccessToken (called by ThinkingProxy.forwardToGrok) attaches a fresh
bearer token for both Grok Composer and Grok Build model requests and that
credentials are stored in ~/.cli-proxy-api/grok-cli.json with type: grok-cli.
- Around line 116-117: Update the two documentation lines referencing Grok so
they include both Grok models added in the PR: mention both
"grok-composer-2.5-fast" (Composer 2.5) and "grok-build" (Grok Build) in the
descriptions for src/Sources/ThinkingProxy.swift and
src/Sources/DroidProxyModelCatalog.swift; ensure the wording mirrors
CHANGELOG.md's line 13 by adding "Grok Build (`grok-build`)" alongside the
existing Composer 2.5 entry so both model names and their beta-gated status are
documented consistently.

In `@README.md`:
- Line 66: Update the README entry for GrokAuth.swift to reflect that it handles
authentication for both Grok models; change the inline comment text that
currently references only "Composer 2.5" to mention both "Composer 2.5 and Grok
Build" so it matches CHANGELOG.md and the GrokAuth.swift responsibility.

---

Nitpick comments:
In `@src/Sources/GrokAuth.swift`:
- Line 173: Replace the current ternary that force-unwraps creds.email with a
safe, simplified expression that treats nil and empty strings the same and
avoids force-unwrapping; for example, compute an email value by checking
creds.email?.isEmpty with the nil-coalescing fallback and then use that variable
in the payload instead of the existing "(creds.email?.isEmpty == false ?
creds.email! : "grok-user")" expression in GrokAuth (the dictionary/payload
construction referencing creds.email).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b1970933-34e9-45e1-b6a8-adcb1c286318

📥 Commits

Reviewing files that changed from the base of the PR and between 97a45ca and c72f769.

⛔ Files ignored due to path filters (1)
  • src/Sources/Resources/icon-grok.svg is excluded by !**/*.svg
📒 Files selected for processing (12)
  • AGENTS.md
  • CHANGELOG.md
  • README.md
  • src/Sources/AuthStatus.swift
  • src/Sources/DroidProxyModelCatalog.swift
  • src/Sources/GrokAuth.swift
  • src/Sources/ServerManager.swift
  • src/Sources/SettingsView.swift
  • src/Sources/ThinkingProxy.swift
  • src/Tests/CLIProxyMenuBarTests/DroidProxyModelCatalogTests.swift
  • src/Tests/CLIProxyMenuBarTests/GrokAuthTests.swift
  • src/Tests/CLIProxyMenuBarTests/ThinkingProxyGrokConvIDTests.swift

Comment thread AGENTS.md
Comment on lines +116 to +117
| `src/Sources/ThinkingProxy.swift` | Raw TCP HTTP proxy that forwards requests to CLIProxyAPI. Rewrites the Anthropic-Beta header to drop `redact-thinking-2026-02-12` on Claude thinking requests, injects `service_tier=priority` on enabled Codex fast-mode models, rewrites OAuth Code Assist Gemini `/v1/responses` to `/v1/chat/completions`, and emits a `REQUEST REASONING` log line per request. Does not inject reasoning or thinking fields. Direct-forwards `cursor-` models to the Cursor proxy and `grok-` models (Composer 2.5) to `cli-chat-proxy.grok.com` with an xAI OAuth bearer plus `x-xai-token-auth`/`x-grok-*` headers (see `GrokAuth`); both paths are beta-gated. |
| `src/Sources/DroidProxyModelCatalog.swift` | Authoritative catalog of DroidProxy-exposed models. Each `DroidProxyModelDefinition` carries its supported `levels` plus a `defaultLevelValue`, and `settingsEntry` always embeds Factory's native reasoning metadata (`enableThinking`, `supportedReasoningEfforts`, `defaultReasoningEffort`, `reasoningEffort`) so Droid CLI's per-session selector can expose the full level set. Beta-gated entries include the Cursor and Grok CLI (Composer 2.5, `grok-composer-2.5-fast`) providers. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Document both Grok models, not just Composer 2.5.

Both lines reference only Composer 2.5 (grok-composer-2.5-fast) but the PR adds two Grok models: Composer 2.5 and Grok Build (grok-build). Line 13 in CHANGELOG.md correctly lists both. Update these descriptions to include both models for consistency.

📝 Suggested fix
-| `src/Sources/ThinkingProxy.swift` | Raw TCP HTTP proxy that forwards requests to CLIProxyAPI. Rewrites the Anthropic-Beta header to drop `redact-thinking-2026-02-12` on Claude thinking requests, injects `service_tier=priority` on enabled Codex fast-mode models, rewrites OAuth Code Assist Gemini `/v1/responses` to `/v1/chat/completions`, and emits a `REQUEST REASONING` log line per request. Does not inject reasoning or thinking fields. Direct-forwards `cursor-` models to the Cursor proxy and `grok-` models (Composer 2.5) to `cli-chat-proxy.grok.com` with an xAI OAuth bearer plus `x-xai-token-auth`/`x-grok-*` headers (see `GrokAuth`); both paths are beta-gated. |
+| `src/Sources/ThinkingProxy.swift` | Raw TCP HTTP proxy that forwards requests to CLIProxyAPI. Rewrites the Anthropic-Beta header to drop `redact-thinking-2026-02-12` on Claude thinking requests, injects `service_tier=priority` on enabled Codex fast-mode models, rewrites OAuth Code Assist Gemini `/v1/responses` to `/v1/chat/completions`, and emits a `REQUEST REASONING` log line per request. Does not inject reasoning or thinking fields. Direct-forwards `cursor-` models to the Cursor proxy and `grok-` models (Composer 2.5, Grok Build) to `cli-chat-proxy.grok.com` with an xAI OAuth bearer plus `x-xai-token-auth`/`x-grok-*` headers (see `GrokAuth`); both paths are beta-gated. |
-| `src/Sources/DroidProxyModelCatalog.swift` | Authoritative catalog of DroidProxy-exposed models. Each `DroidProxyModelDefinition` carries its supported `levels` plus a `defaultLevelValue`, and `settingsEntry` always embeds Factory's native reasoning metadata (`enableThinking`, `supportedReasoningEfforts`, `defaultReasoningEffort`, `reasoningEffort`) so Droid CLI's per-session selector can expose the full level set. Beta-gated entries include the Cursor and Grok CLI (Composer 2.5, `grok-composer-2.5-fast`) providers. |
+| `src/Sources/DroidProxyModelCatalog.swift` | Authoritative catalog of DroidProxy-exposed models. Each `DroidProxyModelDefinition` carries its supported `levels` plus a `defaultLevelValue`, and `settingsEntry` always embeds Factory's native reasoning metadata (`enableThinking`, `supportedReasoningEfforts`, `defaultReasoningEffort`, `reasoningEffort`) so Droid CLI's per-session selector can expose the full level set. Beta-gated entries include the Cursor and Grok CLI (Composer 2.5, Grok Build) providers. |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` around lines 116 - 117, Update the two documentation lines
referencing Grok so they include both Grok models added in the PR: mention both
"grok-composer-2.5-fast" (Composer 2.5) and "grok-build" (Grok Build) in the
descriptions for src/Sources/ThinkingProxy.swift and
src/Sources/DroidProxyModelCatalog.swift; ensure the wording mirrors
CHANGELOG.md's line 13 by adding "Grok Build (`grok-build`)" alongside the
existing Composer 2.5 entry so both model names and their beta-gated status are
documented consistently.

Comment thread AGENTS.md
| `src/Sources/DroidProxyModelCatalog.swift` | Authoritative catalog of DroidProxy-exposed models. Each `DroidProxyModelDefinition` carries its supported `levels` plus a `defaultLevelValue`, and `settingsEntry` always embeds Factory's native reasoning metadata (`enableThinking`, `supportedReasoningEfforts`, `defaultReasoningEffort`, `reasoningEffort`) so Droid CLI's per-session selector can expose the full level set. Beta-gated entries include the Cursor and Grok CLI (Composer 2.5, `grok-composer-2.5-fast`) providers. |
| `src/Sources/SettingsView.swift` | SwiftUI settings UI for server status, launch-at-login, provider toggles, auth flows, the Codex fast-mode (`service_tier=priority`) subsection, the Factory custom-models Apply button, OLED theme, background opacity, and remote-access settings. No thinking/reasoning selectors — those live in Droid CLI. |
| `src/Sources/AuthStatus.swift` | `AuthManager`, account parsing, expiry detection, file deletion, and per-account disabled-state updates. |
| `src/Sources/GrokAuth.swift` | xAI Grok CLI OAuth 2.0 device-flow login (RFC 8628 against `auth.x.ai`), access-token refresh, and credential storage (`~/.cli-proxy-api/grok-cli.json`, `type: grok-cli`). `ensureValidAccessToken` is called by `ThinkingProxy.forwardToGrok` to attach a fresh bearer token to Composer 2.5 traffic. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update GrokAuth description to cover both Grok models.

Line 120 mentions only "Composer 2.5 traffic" but GrokAuth provides authentication for both Grok models (Composer 2.5 and Grok Build). Update the description to reflect that it handles all grok- model traffic.

📝 Suggested fix
-| `src/Sources/GrokAuth.swift` | xAI Grok CLI OAuth 2.0 device-flow login (RFC 8628 against `auth.x.ai`), access-token refresh, and credential storage (`~/.cli-proxy-api/grok-cli.json`, `type: grok-cli`). `ensureValidAccessToken` is called by `ThinkingProxy.forwardToGrok` to attach a fresh bearer token to Composer 2.5 traffic. |
+| `src/Sources/GrokAuth.swift` | xAI Grok CLI OAuth 2.0 device-flow login (RFC 8628 against `auth.x.ai`), access-token refresh, and credential storage (`~/.cli-proxy-api/grok-cli.json`, `type: grok-cli`). `ensureValidAccessToken` is called by `ThinkingProxy.forwardToGrok` to attach a fresh bearer token to Grok model traffic (Composer 2.5, Grok Build). |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@AGENTS.md` at line 120, Update the AGENTS.md entry for
src/Sources/GrokAuth.swift to say that GrokAuth handles authentication for all
grok- model traffic (both Composer 2.5 and Grok Build) rather than only
"Composer 2.5 traffic"; mention that ensureValidAccessToken (called by
ThinkingProxy.forwardToGrok) attaches a fresh bearer token for both Grok
Composer and Grok Build model requests and that credentials are stored in
~/.cli-proxy-api/grok-cli.json with type: grok-cli.

Comment thread README.md
│ ├── SettingsView.swift # SwiftUI settings UI
│ ├── DroidProxyModelCatalog.swift # Authoritative catalog of exposed Factory models
│ ├── AuthStatus.swift # AuthManager: account parsing, expiry, enable/disable
│ ├── GrokAuth.swift # xAI Grok CLI OAuth device flow + token refresh (Composer 2.5)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Update GrokAuth comment to mention both Grok models.

The inline comment mentions only "Composer 2.5" but GrokAuth handles authentication for both Grok models (Composer 2.5 and Grok Build). Update for consistency with CHANGELOG.md line 13.

📝 Suggested fix
-│   ├── GrokAuth.swift               # xAI Grok CLI OAuth device flow + token refresh (Composer 2.5)
+│   ├── GrokAuth.swift               # xAI Grok CLI OAuth device flow + token refresh (Composer 2.5, Grok Build)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
│ ├── GrokAuth.swift # xAI Grok CLI OAuth device flow + token refresh (Composer 2.5)
│ ├── GrokAuth.swift # xAI Grok CLI OAuth device flow + token refresh (Composer 2.5, Grok Build)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 66, Update the README entry for GrokAuth.swift to reflect
that it handles authentication for both Grok models; change the inline comment
text that currently references only "Composer 2.5" to mention both "Composer 2.5
and Grok Build" so it matches CHANGELOG.md and the GrokAuth.swift
responsibility.

Copy link
Copy Markdown
Owner

Review Triage: 15 Comments Evaluated

Commit 81d5c71 applies the actionable fixes below. Full classification follows.


Applied (4 comments fixed, commit 81d5c71)

#2 — Author note, GrokAuth.swift:284 — isCancelled check before URI open (VALID)

Fixed. The NSWorkspace.shared.open(url) call at line 293 ran unconditionally even if session.cancel() had already been called between the URLSession callback firing and execution reaching that line. Added !session.isCancelled as the first clause of the if let url guard so a cancelled session never launches a browser tab.

#11 — gemini-code-assist, GrokAuth.swift:173 — Force-unwrap creds.email! (VALID)

Fixed. The original (creds.email?.isEmpty == false ? creds.email! : "grok-user") pattern is logically safe (the optional is checked before the bang) but the force-unwrap is fragile under refactoring and triggers static analysis warnings. Replaced with (creds.email.flatMap { $0.isEmpty ? nil : $0 } ?? "grok-user"), which is equivalent, shorter, and never force-unwraps.

#12/#13/#14 — coderabbitai, AGENTS.md:117, AGENTS.md:120, README.md:66 — Document both Grok models (VALID)

Fixed in the same commit. The PR adds two models (grok-composer-2.5-fast / Composer 2.5 and grok-build / Grok Build) but the documentation only mentioned Composer 2.5. Updated:

  • AGENTS.md Auth And Providers: changed "four" to "five" and added the grok entry with both models.
  • AGENTS.md Key Files table: DroidProxyModelCatalog.swift and GrokAuth.swift rows now name both models.
  • README.md project tree: GrokAuth.swift and ThinkingProxy.swift inline comments now include both models.

Dismissed — Off-base (8 comments, all bot-generated)

#3/#4/#5/#8/#9/#10 — gemini-code-assist HIGH/MEDIUM: JSON integer cast to Double (OFF-BASE)

These six comments (three HIGH + three MEDIUM duplicates) flag (obj["interval"] as? Double), (obj["expires_in"] as? Double), and (json["expires"] as? Double) ?? 0 as broken because "JSON integers cannot be cast to Double."

This is incorrect for Swift's JSONSerialization path. JSONSerialization returns numeric values as NSNumber regardless of whether the JSON source token has a decimal point. Swift's as? operator on an NSNumber to Double bridges via NSNumber.doubleValue and always succeeds — there is no integer/float distinction at the NSNumber level. The existing unit tests (16 in GrokAuthTests) already exercise the parsing paths with integer-valued fields and pass. No change needed.

#6 — gemini-code-assist MEDIUM: Retain cycle in stateUpdateHandler (OFF-BASE)

The comment says targetConnection is captured strongly and creates a retain cycle. The handler at ThinkingProxy.swift:1259 already reads { [weak self] state in ... }self is already weak. The targetConnection capture is of a local stack variable (a freshly created NWConnection instance), not a property on self, so no object-to-object cycle exists. NWConnection does hold its handler, but the connection is cancelled and released at the end of the request; there is no cycle through self. No change needed.


Dismissed — Skip per triage rules (1 comment)

#7 — gemini-code-assist MEDIUM: Full JSON parse on hot path in grokConversationID (SKIP)

grokConversationID is only called from forwardToGrok, which is already downstream of the isGrokModel guard — it runs only for grok- prefixed models, not on every request. This is not the main hot path. Marking as premature optimization per the triage classification guide.


Discussion — Author's own note (1 comment)

#1 — Author note, GrokAuth.swift:196 — Race condition in loadActiveCredentials without lock

After reviewing the code: loadActiveCredentials has no shared mutable state of its own. All its work is local (best is a function-local variable; reads from disk via Data(contentsOf:) are independent). The shared mutable state (refreshInFlight, refreshWaiters) lives in ensureValidAccessToken and is already protected by refreshLock. The function is called in two places:

  1. From ensureValidAccessToken — the double-checked locking pattern around refreshInFlight means at most one network refresh fires, and loadActiveCredentials is called before and after the lock window only for read purposes.
  2. Directly from ThinkingProxy.forwardToGrok via ensureValidAccessToken.

The concern may be about concurrent reads racing with a persist write. persist uses ioLock and options: .atomic write, so readers either see the old file or the complete new file — no torn reads on macOS APFS. If the intent was to guard against a read returning stale credentials that persist is about to replace, the existing double-read inside ensureValidAccessToken (re-reading after winning refreshInFlight) already covers that. No lock on loadActiveCredentials is needed unless there is a specific scenario I'm missing — flagging for author review.


Summary

# File Classification Action
1 GrokAuth.swift:196 Discussion See analysis above — no lock needed, flagged for author review
2 GrokAuth.swift:284 Actionable Fixed in 81d5c71
3,8 GrokAuth.swift:110 Off-base (duplicate pair) NSNumber bridges to Double correctly
4,9 GrokAuth.swift:128 Off-base (duplicate pair) Same
5,10 GrokAuth.swift:187 Off-base (duplicate pair) Same
6 ThinkingProxy.swift:1260 Off-base [weak self] already present; no cycle
7 ThinkingProxy.swift:1322 Skip Premature optimization on a model-gated path
11 GrokAuth.swift:173 Actionable Fixed in 81d5c71
12 AGENTS.md:117 Actionable Fixed in 81d5c71
13 AGENTS.md:120 Actionable Fixed in 81d5c71
14 README.md:66 Actionable Fixed in 81d5c71

4 comments addressed / 8 dismissed as off-base / 1 skipped / 1 flagged for discussion

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants