Skip to content
Merged
23 changes: 23 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,29 @@ else:
- **Tape-first memory**: Use anchor/handoff to bound context windows and replay full evidence.
- **Event streaming**: Subscribe to text deltas, tool calls, tool results, usage, and final state.

## Provider Auth Resolver

Republic can resolve provider keys dynamically via `api_key_resolver`.

```python
from republic import LLM, login_openai_codex_oauth, openai_codex_oauth_resolver

# First-time login (paste redirect URL when prompted by your app/CLI wrapper).
# You can wire `prompt_for_redirect` to your own input UI.
login_openai_codex_oauth(
prompt_for_redirect=lambda authorize_url: input(f"Open this URL and paste callback URL:\n{authorize_url}\n> "),
)

llm = LLM(
model="openai:gpt-5.3-codex",
api_key_resolver=openai_codex_oauth_resolver(),
)
print(llm.chat("Say hello in one sentence."))
```

`openai_codex_oauth_resolver()` reads `~/.codex/auth.json` (or `$CODEX_HOME/auth.json`) and returns
the current access token for `openai`, refreshing it automatically when it is near expiry.
If you omit `prompt_for_redirect`, login will try to capture the callback from `redirect_uri` automatically.
## Development

```bash
Expand Down
63 changes: 63 additions & 0 deletions examples/06_openai_codex_oauth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

import argparse
import os

from republic import LLM, login_openai_codex_oauth, openai_codex_oauth_resolver


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Authenticate with OpenAI Codex OAuth and run a simple Republic chat.",
)
parser.add_argument(
"--login-only",
action="store_true",
help="Run OAuth login and persist tokens without sending a chat request.",
)
parser.add_argument(
"--model",
default=os.getenv("REPUBLIC_CODEX_MODEL", "openai:gpt-5-codex"),
help="Model to use after login.",
)
parser.add_argument(
"--prompt",
default="Explain tape-first workflows in one sentence.",
help="Prompt to send after login.",
)
return parser.parse_args()


def prompt_for_redirect(authorize_url: str) -> str:
print("Open this URL in your browser and complete the sign-in flow:\n")
print(authorize_url)
print("\nPaste the full callback URL (or the authorization code) here.")
return input("> ").strip()


def main() -> None:
args = parse_args()

tokens = login_openai_codex_oauth(
prompt_for_redirect=None,
)
print("login: ok")
print("account_id:", tokens.account_id or "-")

if args.login_only:
return

llm = LLM(
model=args.model,
api_key_resolver=openai_codex_oauth_resolver(),
)
out = llm.chat(args.prompt)

if out.error:
print("error:", out.error.kind, out.error.message)
return
print("text:", out.value)


if __name__ == "__main__":
main()
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ keywords = [
requires-python = ">=3.11,<4.0"
dependencies = [
"any-llm-sdk>=1.7.0",
"authlib>=1.6.5",
"httpx>=0.28.1",
"pydantic>=2.7.0",
]
classifiers = [
Expand Down
6 changes: 6 additions & 0 deletions src/republic/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"""Republic public API."""

from republic.auth.openai_codex import (
login_openai_codex_oauth,
openai_codex_oauth_resolver,
Copy link
Contributor Author

@niyue niyue Mar 5, 2026

Choose a reason for hiding this comment

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

If this PR is merged, I’ll follow up with a separate PR in bub that uses these two APIs to:

  1. add a CLI login command for Codex OAuth (to generate/store credentials), and
  2. use those OAuth credentials for OpenAI requests.

I’ve already done an initial local validation of this flow in bub using these two APIs.

)
from republic.core.results import (
AsyncStreamEvents,
AsyncTextStream,
Expand Down Expand Up @@ -34,6 +38,8 @@
"ToolAutoResult",
"ToolContext",
"ToolSet",
"login_openai_codex_oauth",
"openai_codex_oauth_resolver",
"schema_from_model",
"tool",
"tool_from_model",
Expand Down
3 changes: 3 additions & 0 deletions src/republic/auth/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Authentication helpers."""

from republic.auth.openai_codex import * # noqa: F403
Loading