feat: add Claude Code OAuth and Codex CLI as LLM providers (#1166)

* feat: add Claude Code OAuth and Codex CLI providers

Port of bytedance/deer-flow#1136 from @solanian's feat/cli-oauth-providers branch.\n\nCarries the feature forward on top of current main without the original CLA-blocked commit metadata, while preserving attribution in the commit message for review.

* fix: harden CLI credential loading

Align Codex auth loading with the current ~/.codex/auth.json shape, make Docker credential mounts directory-based to avoid broken file binds on hosts without exported credential files, and add focused loader tests.

* refactor: tighten codex auth typing

Replace the temporary Any return type in CodexChatModel._load_codex_auth with the concrete CodexCliCredential type after the credential loader was stabilized.

* fix: load Claude Code OAuth from Keychain

Match Claude Code's macOS storage strategy more closely by checking the Keychain-backed credentials store before falling back to ~/.claude/.credentials.json. Keep explicit file overrides and add focused tests for the Keychain path.

* fix: require explicit Claude OAuth handoff

* style: format thread hooks reasoning request

* docs: document CLI-backed auth providers

* fix: address provider review feedback

* fix: harden provider edge cases

* Fix deferred tools, Codex message normalization, and local sandbox paths

* chore: narrow PR scope to OAuth providers

* chore: remove unrelated frontend changes

* chore: reapply OAuth branch frontend scope cleanup

* fix: preserve upload guards with reasoning effort wiring

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
Purricane
2026-03-22 07:39:50 -07:00
committed by GitHub
parent e119dc74ae
commit 835ba041f8
12 changed files with 1546 additions and 0 deletions

View File

@@ -9,6 +9,7 @@ from deerflow.config.app_config import AppConfig
from deerflow.config.model_config import ModelConfig
from deerflow.config.sandbox_config import SandboxConfig
from deerflow.models import factory as factory_module
from deerflow.models import openai_codex_provider as codex_provider_module
# ---------------------------------------------------------------------------
# Helpers
@@ -30,6 +31,7 @@ def _make_model(
supports_reasoning_effort: bool = False,
when_thinking_enabled: dict | None = None,
thinking: dict | None = None,
max_tokens: int | None = None,
) -> ModelConfig:
return ModelConfig(
name=name,
@@ -37,6 +39,7 @@ def _make_model(
description=None,
use=use,
model=name,
max_tokens=max_tokens,
supports_thinking=supports_thinking,
supports_reasoning_effort=supports_reasoning_effort,
when_thinking_enabled=when_thinking_enabled,
@@ -500,6 +503,96 @@ def test_openai_compatible_provider_multiple_models(monkeypatch):
assert captured.get("model") == "MiniMax-M2.5-highspeed"
# ---------------------------------------------------------------------------
# Codex provider reasoning_effort mapping
# ---------------------------------------------------------------------------
class FakeCodexChatModel(FakeChatModel):
pass
def test_codex_provider_disables_reasoning_when_thinking_disabled(monkeypatch):
cfg = _make_app_config(
[
_make_model(
"codex",
use="deerflow.models.openai_codex_provider:CodexChatModel",
supports_thinking=True,
supports_reasoning_effort=True,
)
]
)
_patch_factory(monkeypatch, cfg, model_class=FakeCodexChatModel)
monkeypatch.setattr(codex_provider_module, "CodexChatModel", FakeCodexChatModel)
FakeChatModel.captured_kwargs = {}
factory_module.create_chat_model(name="codex", thinking_enabled=False)
assert FakeChatModel.captured_kwargs.get("reasoning_effort") == "none"
def test_codex_provider_preserves_explicit_reasoning_effort(monkeypatch):
cfg = _make_app_config(
[
_make_model(
"codex",
use="deerflow.models.openai_codex_provider:CodexChatModel",
supports_thinking=True,
supports_reasoning_effort=True,
)
]
)
_patch_factory(monkeypatch, cfg, model_class=FakeCodexChatModel)
monkeypatch.setattr(codex_provider_module, "CodexChatModel", FakeCodexChatModel)
FakeChatModel.captured_kwargs = {}
factory_module.create_chat_model(name="codex", thinking_enabled=True, reasoning_effort="high")
assert FakeChatModel.captured_kwargs.get("reasoning_effort") == "high"
def test_codex_provider_defaults_reasoning_effort_to_medium(monkeypatch):
cfg = _make_app_config(
[
_make_model(
"codex",
use="deerflow.models.openai_codex_provider:CodexChatModel",
supports_thinking=True,
supports_reasoning_effort=True,
)
]
)
_patch_factory(monkeypatch, cfg, model_class=FakeCodexChatModel)
monkeypatch.setattr(codex_provider_module, "CodexChatModel", FakeCodexChatModel)
FakeChatModel.captured_kwargs = {}
factory_module.create_chat_model(name="codex", thinking_enabled=True)
assert FakeChatModel.captured_kwargs.get("reasoning_effort") == "medium"
def test_codex_provider_strips_unsupported_max_tokens(monkeypatch):
cfg = _make_app_config(
[
_make_model(
"codex",
use="deerflow.models.openai_codex_provider:CodexChatModel",
supports_thinking=True,
supports_reasoning_effort=True,
max_tokens=4096,
)
]
)
_patch_factory(monkeypatch, cfg, model_class=FakeCodexChatModel)
monkeypatch.setattr(codex_provider_module, "CodexChatModel", FakeCodexChatModel)
FakeChatModel.captured_kwargs = {}
factory_module.create_chat_model(name="codex", thinking_enabled=True)
assert "max_tokens" not in FakeChatModel.captured_kwargs
def test_openai_responses_api_settings_are_passed_to_chatopenai(monkeypatch):
model = ModelConfig(
name="gpt-5-responses",