Skip to content

Improvements to documentation with regards to remote OAuth issue#97

Open
mojomast wants to merge 3 commits intoMirrowel:mainfrom
mojomast:main
Open

Improvements to documentation with regards to remote OAuth issue#97
mojomast wants to merge 3 commits intoMirrowel:mainfrom
mojomast:main

Conversation

@mojomast
Copy link
Collaborator

@mojomast mojomast commented Jan 22, 2026

Summary

Added comprehensive documentation explaining the OAuth token authentication issue when the proxy runs on remote hosts and the SSH port forwarding solution.

Changes

  • README.md: Added new section "Remote Host Deployment (SSH Port Forwarding)" with detailed problem explanation and solution examples
  • DOCUMENTATION.md: Added section 2.6.4 explaining remote host authentication with SSH port forwarding
  • Deployment guide.md: Updated SSH tunnel commands to match standardized examples
  • Added troubleshooting entry for OAuth callback failures on remote VPS

Details

This documentation addresses the issue where OAuth callbacks to localhost fail on remote VPS deployments because localhost on the VPS refers to the VPS itself, not the user's local machine. The solution uses SSH port forwarding to tunnel OAuth callback ports back to the local machine during credential setup.

Two approaches are documented:

  1. SSH port forwarding for direct VPS authentication
  2. Local authentication + credential export for stateless deployment

Example Usage

SSH Port Forwarding (for adding credentials directly on VPS)

# Forward all three OAuth ports simultaneously
ssh -L 8085:localhost:8085 -L 51121:localhost:51121 -L 11451:localhost:11451 user@your-vps-ip

# Then run credential tool on VPS
python -m rotator_library.credential_tool

Alternative: Local Authentication + Export

  1. Complete OAuth flows locally
  2. Export credentials to environment variables using the credential tool
  3. Deploy .env file to remote server

Important

Improves documentation for handling OAuth callback issues on remote hosts using SSH port forwarding, with updates in README.md, DOCUMENTATION.md, and Deployment guide.md.

  • Documentation Enhancements:
    • README.md: Adds "Remote Host Deployment (SSH Port Forwarding)" section explaining OAuth callback issues and SSH port forwarding solution.
    • DOCUMENTATION.md: Introduces section 2.6.4 on remote host authentication using SSH port forwarding.
    • Deployment guide.md: Updates SSH tunnel commands to standardized examples.
  • Troubleshooting:
    • Adds entry for OAuth callback failures on remote VPS in README.md.

This description was created by Ellipsis for c1e07c0. You can customize this summary. It will automatically update as commits are pushed.

…rt forwarding

Add comprehensive documentation explaining OAuth token authentication
issue when proxy runs on remote hosts and the SSH port forwarding
solution. Updates README.md, DOCUMENTATION.md, and Deployment guide.md
with detailed examples and workflows.

- Added new section in README.md explaining remote host OAuth problem
- Updated DOCUMENTATION.md with section 2.6.4 on SSH port forwarding
- Modified Deployment guide.md to standardize SSH tunnel command ordering
- Added troubleshooting entry for OAuth callback failures on VPS
- Included both SSH tunnel and local authentication export approaches
@mojomast mojomast requested a review from Mirrowel as a code owner January 22, 2026 03:04
@mirrobot-agent
Copy link
Contributor

I'm beginning my review of the documentation improvements for remote OAuth authentication. I'll examine the new SSH port forwarding instructions and the troubleshooting guide to ensure everything is clear and accurate.

Copy link
Contributor

@mirrobot-agent mirrobot-agent bot left a comment

Choose a reason for hiding this comment

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

Overall Assessment

This PR provides excellent and much-needed documentation for handling OAuth authentication on remote hosts. The explanation of why localhost callbacks fail on a VPS and how to use SSH port forwarding as a solution is clear and will save users a lot of troubleshooting time.

Architectural Feedback

The approach of documenting both the SSH tunnel method and the "local auth + export" alternative is great, as it covers different deployment styles (persistent VPS vs. stateless cloud).

Key Suggestions

  • Consistency: I noticed the iFlow port (11451) was missing from a few of the aggregate examples in README.md. Keeping all three ports (8085, 51121, 11451) in the "all-at-once" examples ensures all providers are covered.
  • Headless VPS Context: I suggested a small clarification regarding the "opening browser" behavior, as users on headless servers will typically need to copy-paste a URL manually.
  • Actionability: Added a suggestion to include the specific --export flag in the alternative workflow section to make it easier for users to follow.

Questions for the Author

None. The documentation is very straightforward and helpful.

This review was generated by an AI assistant.


If SSH port forwarding is not feasible:
1. Complete OAuth flows locally on your machine
2. Export credentials to environment variables using credential tool's export feature
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be helpful to include the actual command for exporting credentials here, e.g., python -m rotator_library.credential_tool --export, so users don’t have to hunt for it.

ssh -L 51121:localhost:51121 user@your-vps-ip # For Antigravity
ssh -L 8085:localhost:8085 user@your-vps-ip # For Gemini CLI
ssh -L 11451:localhost:11451 user@your-vps-ip # For iFlow
ssh -L 51121:localhost:51121 user@your-vps-ip # For Antigravity
Copy link
Contributor

Choose a reason for hiding this comment

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

There is a slight formatting inconsistency with the indentation of the comment here (triple space vs single space on other lines).

Suggested change
ssh -L 51121:localhost:51121 user@your-vps-ip # For Antigravity
ssh -L 51121:localhost:51121 user@your-vps-ip # For Antigravity


**Complete Workflow:**

1. **Establish SSH tunnel** (keep this connection open):
Copy link
Contributor

Choose a reason for hiding this comment

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

The iFlow port (11451) is missing from this example, while it is included in the examples just above (line 809). Adding it here would ensure consistency.

Suggested change
1. **Establish SSH tunnel** (keep this connection open):
ssh -L 8085:localhost:8085 -L 51121:localhost:51121 -L 11451:localhost:11451 user@your-vps-ip

python -m rotator_library.credential_tool
```

3. **Complete OAuth authentication**:
Copy link
Contributor

Choose a reason for hiding this comment

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

On many remote servers (especially headless VPS instances), the credential tool cannot automatically open a browser window. It usually prints a URL for the user to copy. Clarifying this (e.g., "The tool will provide a URL to open in your local browser") would be more accurate for VPS users.

| All keys on cooldown | All keys failed recently; check `logs/detailed_logs/` for upstream errors |
| Model not found | Verify format is `provider/model_name` (e.g., `gemini/gemini-2.5-flash`) |
| OAuth callback failed | Ensure callback port (8085, 51121, 11451) isn't blocked by firewall |
| OAuth callback failed on remote VPS | Use SSH port forwarding: `ssh -L 8085:localhost:8085 -L 51121:localhost:51121 user@your-vps-ip` |
Copy link
Contributor

Choose a reason for hiding this comment

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

The iFlow port (11451) is also missing from this troubleshooting entry. Including it would ensure all OAuth providers are covered.

Suggested change
| OAuth callback failed on remote VPS | Use SSH port forwarding: `ssh -L 8085:localhost:8085 -L 51121:localhost:51121 user@your-vps-ip` |
| OAuth callback failed on remote VPS | Use SSH port forwarding: `ssh -L 8085:localhost:8085 -L 51121:localhost:51121 -L 11451:localhost:11451 user@your-vps-ip` |

Mirrowel and others added 2 commits March 5, 2026 15:48
…mini compatibility

This update restructures how JSON schemas are sanitized before being sent to LLM providers to improve model acceptance rates.

- Split validation keywords into specific groups: those stripped only for Claude (which Gemini supports) and those rejected by all models.
- Preserved validation keywords for Gemini that were previously stripped, such as `pattern`, `format`, `minItems`, and `not`.
- Expanded the list of globally stripped meta-keywords to include `$comment`, `$dynamicRef`, and conditional logic (`if`/`then`/`else`) to prevent API errors across all providers.
Implements a custom provider for Hatz AI that translates between OpenAI
Chat Completions format (used by clients like Claude Code) and Hatz's
Responses API (/v1/openai/responses).

Key features:
- Bidirectional message format conversion (Chat Completions <-> Responses API)
- Streaming SSE translation (response.output_text.delta -> delta.content, etc.)
- Tool calling support with proper function_call/function_call_output mapping
- reasoning_effort -> reasoning.effort passthrough for thinking models
- Handles response.incomplete events (maps to finish_reason: length)
- X-API-Key authentication (not Bearer token)
- Dynamic model discovery from /v1/chat/models endpoint

Also fixes:
- Anthropic /v1/messages endpoint now skips auth when PROXY_API_KEY unset
  (matching existing OpenAI endpoint behavior)
- Adds Hatz configuration section to .env.example
@coderabbitai
Copy link

coderabbitai bot commented Mar 5, 2026

📝 Walkthrough

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Hatz AI integration with API key authentication support.
    • Introduced open-access mode option when API key verification is disabled.
  • Documentation

    • Added comprehensive remote host deployment guide with SSH port forwarding examples.
    • Expanded OAuth callback troubleshooting section with remote deployment scenarios.
    • Updated configuration examples and setup instructions for better clarity.

Walkthrough

A new Hatz AI provider implementation is introduced alongside documentation and configuration updates for remote host deployment via SSH port forwarding. The proxy now supports optional API key verification, and schema cleaning logic for the Antigravity provider is enhanced to handle additional keyword groups conditionally based on target model.

Changes

Cohort / File(s) Summary
Configuration & Environment
.env.example
Added Hatz AI section with API key placeholders (HATZ_API_KEY_1, HATZ_API_KEY_2) and authentication guidance.
Documentation
DOCUMENTATION.md, Deployment guide.md, README.md
Introduced comprehensive guidance for remote host deployment using SSH port forwarding to handle OAuth callbacks, including single/multi-provider examples, workflows, and troubleshooting. Reordered port forwarding commands for consistency.
Proxy Authentication
src/proxy_app/main.py
Added guards to skip API key verification when PROXY_API_KEY is not set, enabling optional open-access mode.
Provider Updates
src/rotator_library/providers/antigravity_provider.py
Expanded schema cleaning logic with new keyword-alias groups (validation_keywords_claude_only, validation_keywords_all_models) to conditionally strip keywords based on target model.
New Provider Implementation
src/rotator_library/providers/hatz_provider.py
Introduced full HatzProvider class with model discovery, request/response translation between OpenAI Chat Completions and Hatz Responses API, streaming support, authentication via X-API-Key, and comprehensive error handling.

Sequence Diagram

sequenceDiagram
    participant Client
    participant HatzProvider
    participant Hatz_API
    participant OpenAI_Format

    Client->>HatzProvider: acompletion() with OpenAI format
    HatzProvider->>HatzProvider: Translate messages & tools to Hatz format
    HatzProvider->>Hatz_API: POST /v1/openai/responses (SSE stream)
    Hatz_API-->>HatzProvider: Streaming events (text, function_calls, etc.)
    HatzProvider->>HatzProvider: Parse SSE chunks
    HatzProvider->>OpenAI_Format: Translate to OpenAI streaming chunks
    OpenAI_Format-->>Client: Yield OpenAI ModelResponse chunks
    Hatz_API-->>HatzProvider: Stream complete
    HatzProvider->>Client: Return final completion with usage data
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Poem

🐰 A hop, skip and a provider so new,
Hatz joins the fold with API magic true,
Remote hosts now dance through SSH tunnels bright,
Schema keywords cleaned left and right,
One diff to bind them all—quite the sight! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description provides a comprehensive overview of changes, includes specific file modifications, explains the problem and solution, and contains example usage. However, it does not follow the provided template structure (missing Testing Done section and Checklist items). Restructure the description to match the template format: include a 'Testing Done' section describing how changes were validated, and complete the checklist items for local testing, license headers, and documentation updates.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main focus of the pull request: documentation improvements addressing remote OAuth issues. It is concise, clear, and directly reflects the primary changes in README.md, DOCUMENTATION.md, and Deployment guide.md.
Docstring Coverage ✅ Passed Docstring coverage is 84.21% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

Try Coding Plans. Let us write the prompt for your AI agent so you can ship faster (with fewer bugs).
Share your feedback on Discord.


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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/rotator_library/providers/antigravity_provider.py (2)

746-759: 🧹 Nitpick | 🔵 Trivial

Remove duplicated ownership of examples across keyword sets.

examples is in both meta_keywords and validation_keywords_all_models. Because meta keywords are filtered first (Line [910]), the all-model entry is unreachable and can drift over time.

Also applies to: 791-813

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rotator_library/providers/antigravity_provider.py` around lines 746 -
759, The symbol "examples" is duplicated between meta_keywords and
validation_keywords_all_models making the latter unreachable; remove "examples"
from validation_keywords_all_models (keep it in meta_keywords) and likewise
remove any other duplicated keyword entries in the nearby validation_keywords_*
sets referenced around the same region so ownership is single-source (check the
block where validation_keywords_all_models and related validation_keywords_* are
defined and delete duplicated entries such as "examples" there).

918-923: ⚠️ Potential issue | 🟠 Major

Preserving Gemini-only keys currently skips recursive schema cleaning.

When for_gemini is True, this branch copies value as-is and exits. For structured keys added here (not, prefixItems), nested schemas bypass _clean_claude_schema, so nested $ref/default/examples can leak through unexpectedly.

Proposed fix
         # Strip Claude-only keywords when not targeting Gemini
         if key in validation_keywords_claude_only:
             if for_gemini:
-                # Gemini accepts these - preserve them
-                cleaned[key] = value
+                # Gemini accepts these - preserve keyword, but still clean nested schemas
+                if isinstance(value, dict):
+                    cleaned[key] = _clean_claude_schema(value, for_gemini)
+                elif isinstance(value, list):
+                    cleaned[key] = [
+                        _clean_claude_schema(item, for_gemini)
+                        if isinstance(item, dict)
+                        else item
+                        for item in value
+                    ]
+                else:
+                    cleaned[key] = value
             # For Claude: skip - not supported
             continue

Also applies to: 786-788

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rotator_library/providers/antigravity_provider.py` around lines 918 -
923, The branch that preserves Gemini-only keys in
validation_keywords_claude_only currently copies value as-is and uses continue,
which skips recursive cleaning by _clean_claude_schema and lets nested
$ref/default/examples pass through; update the handling in the loop over keys so
that when for_gemini is True you still recursively clean structured schema
values (e.g., for keys like "not" call _clean_claude_schema(value) and for
"prefixItems" iterate and clean each item) before assigning to cleaned[key], and
only skip/continue immediately for simple scalar keys that do not require
recursion; keep the original skip behavior for Claude (when for_gemini is False)
so those keys are omitted.
♻️ Duplicate comments (2)
README.md (2)

828-830: ⚠️ Potential issue | 🟡 Minor

Browser behavior wording is too absolute for VPS/headless environments.

“The credential tool will open a browser window” is not always true remotely; usually a URL is printed for manual opening on the local machine.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 828 - 830, Rewrite the absolute sentence "The
credential tool will open a browser window" to a conditional/softer wording that
covers headless/VPS cases (e.g., "The credential tool will attempt to open a
browser window; on headless or remote systems it will instead print a URL for
you to open manually on your local machine"), and add a short note after the
callback sentence clarifying that SSH-tunneled callbacks are forwarded to the
local machine and may require copying the printed URL if no GUI is available.

816-819: ⚠️ Potential issue | 🟡 Minor

Include iFlow port (11451) in the workflow and troubleshooting tunnel commands.

Both commands currently forward only 8085 and 51121, but iFlow callbacks use 11451, so remote OAuth for iFlow can still fail with the provided commands.

🛠️ Suggested doc correction
-   ssh -L 8085:localhost:8085 -L 51121:localhost:51121 user@your-vps-ip
+   ssh -L 8085:localhost:8085 -L 51121:localhost:51121 -L 11451:localhost:11451 user@your-vps-ip
-| OAuth callback failed on remote VPS | Use SSH port forwarding: `ssh -L 8085:localhost:8085 -L 51121:localhost:51121 user@your-vps-ip` |
+| OAuth callback failed on remote VPS | Use SSH port forwarding: `ssh -L 8085:localhost:8085 -L 51121:localhost:51121 -L 11451:localhost:11451 user@your-vps-ip` |

Also applies to: 1043-1043

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 816 - 819, Update the SSH tunnel instructions to also
forward the iFlow callback port 11451 so remote OAuth works: add an additional
-L 11451:localhost:11451 to the existing ssh tunnel examples (the lines that
currently show -L 8085:localhost:8085 -L 51121:localhost:51121) and mirror this
change in the troubleshooting tunnel command(s); keep the note about keeping the
connection open and mention that 11451 is required for iFlow callbacks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@DOCUMENTATION.md`:
- Line 240: The subsection "2.6.4. Remote Host Authentication (SSH Port
Forwarding)" is placed before "2.6.3", breaking the document order; locate the
heading text "2.6.4. Remote Host Authentication (SSH Port Forwarding)" and
either renumber it to "2.6.3" (and adjust any subsequent section numbers) or
move that entire subsection below the existing "2.6.3" entry so numbering is
sequential, and update any in-doc cross-references that point to "2.6.4"
accordingly.

In `@README.md`:
- Around line 816-823: Add a blank line before each fenced code block under the
numbered list items "Establish SSH tunnel" and "Run the credential tool on the
VPS" so the Markdown has an empty line immediately above each ```bash fence;
update the two list entries that contain the code fences to insert a blank line
before their opening ```bash to satisfy MD031 (blanks-around-fences).
- Line 1028: The link fragment "#remote-host-deployment-ssh-port-forwarding"
points to a <summary> text (not a true heading) so it doesn't resolve; fix by
making the target a real anchor: either convert the existing <summary> block
"Remote Host Deployment (SSH Port Forwarding)" into a proper Markdown heading
(e.g., prefix with "##") or add an explicit HTML anchor immediately before that
section (e.g., <a name="remote-host-deployment-ssh-port-forwarding"></a>), then
update/keep the link text "See the [Remote Host Deployment (SSH Port
Forwarding)](`#remote-host-deployment-ssh-port-forwarding`)..." to point to that
valid anchor.

In `@src/proxy_app/main.py`:
- Around line 706-708: The current check silently allows open access when
PROXY_API_KEY is empty (the conditional using PROXY_API_KEY with "if not
PROXY_API_KEY: return x_api_key or auth"), which must be changed to require an
explicit opt-in flag; update this branch to only bypass authentication when a
dedicated environment flag (e.g., PROXY_ALLOW_UNAUTHENTICATED or
PROXY_AUTH_DISABLED) is set to a truthy value, otherwise treat a missing
PROXY_API_KEY as a configuration error and deny/require authentication (return a
failure rather than implicitly returning x_api_key or auth); locate and modify
the conditional that references PROXY_API_KEY, x_api_key, and auth to enforce
the explicit opt-in flag check and add a clear error/deny path when
PROXY_API_KEY is unset and the opt-in is not enabled.

In `@src/rotator_library/providers/hatz_provider.py`:
- Around line 73-75: The current parsing uses split("/")[-1] which drops path
segments; instead keep the full model ID for routing and dedup by setting
model_name to the full model string (not the last segment) and still append
models using the existing logic (models.append(model if "/" in model else
f"hatz/{model}")); for environment variable keys, derive a safe identifier from
the full model (e.g., replace "/" with "_" or another sanitizer) and add that
sanitized string to env_var_ids so uniqueness is preserved while producing a
valid env var name — update the references to model_name, models.append(...),
and env_var_ids.add(...) accordingly.

---

Outside diff comments:
In `@src/rotator_library/providers/antigravity_provider.py`:
- Around line 746-759: The symbol "examples" is duplicated between meta_keywords
and validation_keywords_all_models making the latter unreachable; remove
"examples" from validation_keywords_all_models (keep it in meta_keywords) and
likewise remove any other duplicated keyword entries in the nearby
validation_keywords_* sets referenced around the same region so ownership is
single-source (check the block where validation_keywords_all_models and related
validation_keywords_* are defined and delete duplicated entries such as
"examples" there).
- Around line 918-923: The branch that preserves Gemini-only keys in
validation_keywords_claude_only currently copies value as-is and uses continue,
which skips recursive cleaning by _clean_claude_schema and lets nested
$ref/default/examples pass through; update the handling in the loop over keys so
that when for_gemini is True you still recursively clean structured schema
values (e.g., for keys like "not" call _clean_claude_schema(value) and for
"prefixItems" iterate and clean each item) before assigning to cleaned[key], and
only skip/continue immediately for simple scalar keys that do not require
recursion; keep the original skip behavior for Claude (when for_gemini is False)
so those keys are omitted.

---

Duplicate comments:
In `@README.md`:
- Around line 828-830: Rewrite the absolute sentence "The credential tool will
open a browser window" to a conditional/softer wording that covers headless/VPS
cases (e.g., "The credential tool will attempt to open a browser window; on
headless or remote systems it will instead print a URL for you to open manually
on your local machine"), and add a short note after the callback sentence
clarifying that SSH-tunneled callbacks are forwarded to the local machine and
may require copying the printed URL if no GUI is available.
- Around line 816-819: Update the SSH tunnel instructions to also forward the
iFlow callback port 11451 so remote OAuth works: add an additional -L
11451:localhost:11451 to the existing ssh tunnel examples (the lines that
currently show -L 8085:localhost:8085 -L 51121:localhost:51121) and mirror this
change in the troubleshooting tunnel command(s); keep the note about keeping the
connection open and mention that 11451 is required for iFlow callbacks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: c3c763f7-282e-4f72-a5b1-80009cf25c1b

📥 Commits

Reviewing files that changed from the base of the PR and between c18dfdd and 3d5adef.

📒 Files selected for processing (7)
  • .env.example
  • DOCUMENTATION.md
  • Deployment guide.md
  • README.md
  • src/proxy_app/main.py
  • src/rotator_library/providers/antigravity_provider.py
  • src/rotator_library/providers/hatz_provider.py
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Greptile Review
🧰 Additional context used
🧬 Code graph analysis (1)
src/rotator_library/providers/hatz_provider.py (4)
src/rotator_library/providers/provider_interface.py (1)
  • ProviderInterface (71-711)
src/rotator_library/model_definitions.py (1)
  • get_all_provider_models (108-111)
src/rotator_library/timeout_config.py (1)
  • streaming (76-89)
src/rotator_library/transaction_logger.py (3)
  • ProviderLogger (472-582)
  • log_error (539-547)
  • log_response_chunk (521-528)
🪛 markdownlint-cli2 (0.21.0)
README.md

[warning] 817-817: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 822-822: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 1028-1028: Link fragments should be valid

(MD051, link-fragments)

🪛 Ruff (0.15.2)
src/rotator_library/providers/hatz_provider.py

[warning] 28-28: Import from collections.abc instead: AsyncGenerator

Import from collections.abc

(UP035)


[warning] 28-28: typing.List is deprecated, use list instead

(UP035)


[warning] 28-28: typing.Dict is deprecated, use dict instead

(UP035)


[warning] 50-50: Missing return type annotation for special method __init__

Add return type annotation: None

(ANN204)


[warning] 77-77: Logging statement uses f-string

(G004)


[warning] 96-97: Logging statement uses f-string

(G004)


[warning] 100-100: Logging statement uses f-string

(G004)


[warning] 101-101: Do not catch blind exception: Exception

(BLE001)


[warning] 102-102: Logging statement uses f-string

(G004)


[warning] 244-244: Missing type annotation for **kwargs

(ANN003)


[warning] 594-594: Missing return type annotation for private function _get

(ANN202)


[warning] 703-703: Missing type annotation for **kwargs

(ANN003)


[warning] 704-704: Use X | Y for type annotations

Convert to X | Y

(UP007)


[warning] 720-720: Missing return type annotation for private function make_request

(ANN202)


[warning] 740-740: Logging statement uses f-string

(G004)


[warning] 750-750: Missing return type annotation for private function stream_handler

(ANN202)


[warning] 773-778: Abstract raise to an inner function

(TRY301)


[warning] 773-778: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 780-785: Abstract raise to an inner function

(TRY301)


[warning] 780-785: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 791-795: Abstract raise to an inner function

(TRY301)


[warning] 791-795: Avoid specifying long messages outside the exception class

(TRY003)


[warning] 817-817: Logging statement uses f-string

(G004)


[warning] 832-832: Logging .exception(...) should be used instead of .error(..., exc_info=True)

(G201)


[warning] 833-833: Logging statement uses f-string

(G004)


[warning] 837-837: Missing return type annotation for private function logging_stream_wrapper

(ANN202)


[warning] 853-853: Missing return type annotation for private function non_stream_wrapper

(ANN202)

🔇 Additional comments (3)
.env.example (1)

55-60: Hatz env configuration docs look clear and consistent.

This section correctly documents key naming, endpoint usage, and X-API-Key auth behavior.

Deployment guide.md (1)

526-533: SSH tunnel examples are now clearer and consistently ordered.

The combined and per-port examples remain correct and improve readability for remote OAuth setup.

src/rotator_library/providers/hatz_provider.py (1)

329-497: Solid SSE event translation coverage.

The mapping for response.created, text/tool deltas, and terminal usage/finish chunks is well structured and practical for Chat Completions compatibility.

- This is the key to "Stateless Deployment" for platforms like Railway, Render, Heroku
- Credentials are referenced internally using `env://` URIs (e.g., `env://gemini_cli/1`)

#### 2.6.4. Remote Host Authentication (SSH Port Forwarding)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Section numbering is out of order.

2.6.4 appears before 2.6.3, which breaks doc structure and cross-reference clarity. Please renumber/reorder this subsection sequence.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@DOCUMENTATION.md` at line 240, The subsection "2.6.4. Remote Host
Authentication (SSH Port Forwarding)" is placed before "2.6.3", breaking the
document order; locate the heading text "2.6.4. Remote Host Authentication (SSH
Port Forwarding)" and either renumber it to "2.6.3" (and adjust any subsequent
section numbers) or move that entire subsection below the existing "2.6.3" entry
so numbering is sequential, and update any in-doc cross-references that point to
"2.6.4" accordingly.

Comment on lines +816 to +823
1. **Establish SSH tunnel** (keep this connection open):
```bash
ssh -L 8085:localhost:8085 -L 51121:localhost:51121 user@your-vps-ip
```

2. **Run the credential tool on the VPS** (in a separate terminal or SSH session):
```bash
ssh user@your-vps-ip
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add blank lines before fenced code blocks in list items.

This section triggers MD031 (blanks-around-fences). Insert a blank line before each ```bash fence under the numbered steps.

🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 817-817: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)


[warning] 822-822: Fenced code blocks should be surrounded by blank lines

(MD031, blanks-around-fences)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` around lines 816 - 823, Add a blank line before each fenced code
block under the numbered list items "Establish SSH tunnel" and "Run the
credential tool on the VPS" so the Markdown has an empty line immediately above
each ```bash fence; update the two list entries that contain the code fences to
insert a blank line before their opening ```bash to satisfy MD031
(blanks-around-fences).

```

See [VPS Deployment](Deployment%20guide.md#appendix-deploying-to-a-custom-vps) for complete guide.
See the [Remote Host Deployment (SSH Port Forwarding)](#remote-host-deployment-ssh-port-forwarding) section above for detailed OAuth setup instructions.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

The fragment link target is invalid (MD051).

#remote-host-deployment-ssh-port-forwarding does not resolve reliably because it points to <summary> text, not a markdown heading anchor.

🔗 Suggested fix
-See the [Remote Host Deployment (SSH Port Forwarding)](`#remote-host-deployment-ssh-port-forwarding`) section above for detailed OAuth setup instructions.
+See the **Remote Host Deployment (SSH Port Forwarding)** section above for detailed OAuth setup instructions.
📝 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
See the [Remote Host Deployment (SSH Port Forwarding)](#remote-host-deployment-ssh-port-forwarding) section above for detailed OAuth setup instructions.
See the **Remote Host Deployment (SSH Port Forwarding)** section above for detailed OAuth setup instructions.
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 1028-1028: Link fragments should be valid

(MD051, link-fragments)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 1028, The link fragment
"#remote-host-deployment-ssh-port-forwarding" points to a <summary> text (not a
true heading) so it doesn't resolve; fix by making the target a real anchor:
either convert the existing <summary> block "Remote Host Deployment (SSH Port
Forwarding)" into a proper Markdown heading (e.g., prefix with "##") or add an
explicit HTML anchor immediately before that section (e.g., <a
name="remote-host-deployment-ssh-port-forwarding"></a>), then update/keep the
link text "See the [Remote Host Deployment (SSH Port
Forwarding)](`#remote-host-deployment-ssh-port-forwarding`)..." to point to that
valid anchor.

Comment on lines +706 to +708
# If PROXY_API_KEY is not set or empty, skip verification (open access)
if not PROXY_API_KEY:
return x_api_key or auth
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fail-open auth here should be explicit opt-in, not implicit.

Line 706 silently disables endpoint authentication when PROXY_API_KEY is unset/empty. A missing env var then exposes Anthropic endpoints without auth.

🔐 Suggested fix (explicit unauthenticated mode)
 async def verify_anthropic_api_key(
     x_api_key: str = Depends(anthropic_api_key_header),
     auth: str = Depends(api_key_header),
 ):
@@
-    # If PROXY_API_KEY is not set or empty, skip verification (open access)
-    if not PROXY_API_KEY:
-        return x_api_key or auth
+    allow_unauthenticated = (
+        os.getenv("ALLOW_UNAUTHENTICATED", "false").lower() == "true"
+    )
+    if not PROXY_API_KEY:
+        if allow_unauthenticated:
+            return x_api_key or auth
+        raise HTTPException(
+            status_code=500,
+            detail="Server misconfigured: PROXY_API_KEY is not set",
+        )
📝 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
# If PROXY_API_KEY is not set or empty, skip verification (open access)
if not PROXY_API_KEY:
return x_api_key or auth
async def verify_anthropic_api_key(
x_api_key: str = Depends(anthropic_api_key_header),
auth: str = Depends(api_key_header),
):
allow_unauthenticated = (
os.getenv("ALLOW_UNAUTHENTICATED", "false").lower() == "true"
)
if not PROXY_API_KEY:
if allow_unauthenticated:
return x_api_key or auth
raise HTTPException(
status_code=500,
detail="Server misconfigured: PROXY_API_KEY is not set",
)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/proxy_app/main.py` around lines 706 - 708, The current check silently
allows open access when PROXY_API_KEY is empty (the conditional using
PROXY_API_KEY with "if not PROXY_API_KEY: return x_api_key or auth"), which must
be changed to require an explicit opt-in flag; update this branch to only bypass
authentication when a dedicated environment flag (e.g.,
PROXY_ALLOW_UNAUTHENTICATED or PROXY_AUTH_DISABLED) is set to a truthy value,
otherwise treat a missing PROXY_API_KEY as a configuration error and
deny/require authentication (return a failure rather than implicitly returning
x_api_key or auth); locate and modify the conditional that references
PROXY_API_KEY, x_api_key, and auth to enforce the explicit opt-in flag check and
add a clear error/deny path when PROXY_API_KEY is unset and the opt-in is not
enabled.

Comment on lines +73 to +75
model_name = model.split("/")[-1] if "/" in model else model
models.append(model if "/" in model else f"hatz/{model}")
env_var_ids.add(model_name)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Model parsing drops path segments and can send the wrong model ID upstream.

Using split("/")[-1] truncates nested model IDs. For example, hatz/openai/gpt-4o becomes gpt-4o, which can break routing and also weakens deduplication in model discovery.

🧩 Suggested fix
-                model_name = model.split("/")[-1] if "/" in model else model
+                model_name = model[len("hatz/"):] if model.startswith("hatz/") else model
                 models.append(model if "/" in model else f"hatz/{model}")
                 env_var_ids.add(model_name)
-            model_name = model.split("/")[-1] if "/" in model else model
+            model_name = model[len("hatz/"):] if model.startswith("hatz/") else model
             kwargs_with_stripped_model = {**kwargs, "model": model_name}

Also applies to: 723-724

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/rotator_library/providers/hatz_provider.py` around lines 73 - 75, The
current parsing uses split("/")[-1] which drops path segments; instead keep the
full model ID for routing and dedup by setting model_name to the full model
string (not the last segment) and still append models using the existing logic
(models.append(model if "/" in model else f"hatz/{model}")); for environment
variable keys, derive a safe identifier from the full model (e.g., replace "/"
with "_" or another sanitizer) and add that sanitized string to env_var_ids so
uniqueness is preserved while producing a valid env var name — update the
references to model_name, models.append(...), and env_var_ids.add(...)
accordingly.

@greptile-apps
Copy link

greptile-apps bot commented Mar 5, 2026

Greptile Summary

This PR is primarily a documentation improvement addressing the OAuth callback failure that occurs when running the credential tool on a remote VPS — it adds a new "Remote Host Deployment (SSH Port Forwarding)" section to README.md and DOCUMENTATION.md, and standardises the SSH command ordering in Deployment guide.md. Beyond docs, it also ships a new HatzProvider (translating OpenAI Chat Completions to Hatz's Responses API), extends JSON Schema keyword stripping in antigravity_provider.py, and aligns the verify_anthropic_api_key open-access guard with the existing pattern in verify_api_key.

Key observations:

  • The SSH -L port forwarding direction is correctly documented: forwarding local ports to the VPS allows the user's local browser to reach the VPS-side OAuth callback server.
  • Two locations in README.md (the "Complete Workflow" step 1 at line 818 and the troubleshooting table at line 1043) omit iFlow port 11451, creating an inconsistency with every other occurrence in the PR (lines 811, 1003, and DOCUMENTATION.md).
  • HatzProvider._build_responses_payload silently drops max_tokens values below 4096 with no warning, which can cause responses to exceed the caller's intended token limit without any operator visibility.
  • examples appears in both meta_keywords and validation_keywords_all_models in antigravity_provider.py — redundant but functionally harmless.

Confidence Score: 2/5

  • Documentation inconsistencies and a silent token-limit override in HatzProvider need fixing before merge.
  • Two locations in README.md omit the iFlow OAuth port (11451) in otherwise-complete SSH forwarding examples, creating inconsistency that would cause OAuth failures for iFlow users. The HatzProvider silently ignores caller-specified max_tokens values below 4096 without logging, creating a silent failure mode where responses exceed intended limits with no operator visibility. All three issues are straightforward to fix and do not require architectural changes.
  • README.md (lines 818 and 1043), src/rotator_library/providers/hatz_provider.py (lines 277-280)

Sequence Diagram

sequenceDiagram
    participant LB as Local Browser
    participant LM as Local Machine (SSH -L)
    participant VPS as Remote VPS
    participant OP as OAuth Provider

    Note over LM,VPS: SSH tunnel established:<br/>ssh -L 8085:localhost:8085 -L 51121:localhost:51121 -L 11451:localhost:11451 user@vps

    VPS->>VPS: python -m rotator_library.credential_tool
    VPS->>LB: Open browser / display auth URL
    LB->>OP: User visits OAuth URL
    OP-->>LB: Redirect to http://localhost:PORT/callback
    LB->>LM: Connect to localhost:PORT
    LM->>VPS: SSH tunnel forwards to VPS:PORT
    VPS->>VPS: Credential tool captures OAuth callback
    VPS->>VPS: Credentials saved to .env / credential store
Loading

Last reviewed commit: 3d5adef


1. **Establish SSH tunnel** (keep this connection open):
```bash
ssh -L 8085:localhost:8085 -L 51121:localhost:51121 user@your-vps-ip
Copy link

Choose a reason for hiding this comment

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

The "Complete Workflow" step 1 SSH tunnel command is missing the iFlow port 11451. This is inconsistent with every other section in the PR (the "Multiple Providers at Once" block at line 811, the "Custom VPS / Systemd" section at line 1003, and DOCUMENTATION.md section 2.6.4).

A user following this workflow who also needs iFlow credentials won't have that port forwarded, causing their OAuth callback to silently fail.

Suggested change
ssh -L 8085:localhost:8085 -L 51121:localhost:51121 user@your-vps-ip
ssh -L 8085:localhost:8085 -L 51121:localhost:51121 -L 11451:localhost:11451 user@your-vps-ip

| All keys on cooldown | All keys failed recently; check `logs/detailed_logs/` for upstream errors |
| Model not found | Verify format is `provider/model_name` (e.g., `gemini/gemini-2.5-flash`) |
| OAuth callback failed | Ensure callback port (8085, 51121, 11451) isn't blocked by firewall |
| OAuth callback failed on remote VPS | Use SSH port forwarding: `ssh -L 8085:localhost:8085 -L 51121:localhost:51121 user@your-vps-ip` |
Copy link

Choose a reason for hiding this comment

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

The troubleshooting table entry for "OAuth callback failed on remote VPS" is also missing iFlow port 11451. This is inconsistent with the "Multiple Providers at Once" command block immediately above (line 811), which correctly lists all three ports.

Suggested change
| OAuth callback failed on remote VPS | Use SSH port forwarding: `ssh -L 8085:localhost:8085 -L 51121:localhost:51121 user@your-vps-ip` |
| OAuth callback failed on remote VPS | Use SSH port forwarding: `ssh -L 8085:localhost:8085 -L 51121:localhost:51121 -L 11451:localhost:11451 user@your-vps-ip` |

Comment on lines +277 to +280
if "max_tokens" in kwargs and kwargs["max_tokens"] is not None:
max_tokens = kwargs["max_tokens"]
if max_tokens >= 4096:
payload["max_output_tokens"] = max_tokens
Copy link

Choose a reason for hiding this comment

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

When a caller explicitly sets max_tokens to any value under 4096, the parameter is silently discarded with no warning logged. The code comment at line 275 documents the constraint, but callers receive no indication that their token limit was not honored.

This creates a silent failure mode: a caller requesting a 512-token limit will receive a response using Hatz's default, which can be significantly larger. Without a warning, operators won't detect this mismatch.

At minimum, log a warning when the value is dropped to alert operators to the constraint.

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