Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

## What You Get

- **53 ToS-friendly providers. 1.3B+ free tokens every month.** Use free, paid, subscription, and local models from one searchable UI without putting your account at risk. FCC follows provider terms and removes integrations if they stop being allowed.
- **54 ToS-friendly providers. 1.3B+ free tokens every month.** Use free, paid, subscription, and local models from one searchable UI without putting your account at risk. FCC follows provider terms and removes integrations if they stop being allowed.
- **10 coding agents. One model catalog.** Run [Claude Code](https://code.claude.com/docs/en/overview), [Codex](https://github.com/openai/codex), [Pi](https://github.com/earendil-works/pi), [OpenCode](https://github.com/anomalyco/opencode), [Cline](https://github.com/cline/cline), [Hermes](https://github.com/NousResearch/hermes-agent), [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness), [Grok Build](https://github.com/xai-org/grok-build), [Muse Code](https://research.meta.ai/blog/introducing-muse-code-and-muse-spark-1-2/), or [Aider](https://aider.chat/) with your FCC models.
- **Keep coding through provider outages.** After retries are exhausted, FCC automatically tries your next configured model without making you restart the turn—across every client.
- **Up to 90% fewer terminal-output tokens.** Optional [RTK](https://github.com/rtk-ai/rtk) filters common command output, while five FCC optimizations handle quota probes, command-prefix detection, titles, suggestions, and filepaths without calling a provider.
Expand Down Expand Up @@ -190,8 +190,8 @@ fcc-aider
## Choose A Provider

1. Open a provider link below for its key, models, or setup instructions.
2. In the Admin UI, configure the listed setting. For OpenAI, use
**Providers → Connected accounts** instead.
2. In the Admin UI, configure the listed setting. For OpenAI / ChatGPT
subscription access, use **Providers → OAuth providers** instead.
3. Search the `MODEL` dropdown and select a model. If the provider cannot list
models, enter `<provider-id>/<exact-provider-model-id>` manually.
4. Click **Apply**.
Expand All @@ -210,6 +210,7 @@ from more than one provider before succeeding.
| [Groq](https://console.groq.com/keys) | `GROQ_API_KEY` | `groq/llama-3.3-70b-versatile` |
| [ClinePass](https://docs.cline.bot/getting-started/clinepass) | `CLINE_API_KEY` | `cline_pass/cline-pass/kimi-k3` |
| [OpenAI / ChatGPT](https://learn.chatgpt.com/docs/auth) | Connect ChatGPT in the Admin UI | `openai/<model-id>` |
| [OpenAI API](https://platform.openai.com/api-keys) | `OPENAI_API_KEY` | `openai_api/gpt-5.6-sol` |
| [GitHub Copilot](https://docs.github.com/en/copilot/how-tos/copilot-sdk/auth/authenticate) | Connect GitHub Copilot in the Admin UI | `github_copilot/<model-id>` |
| [xAI (Grok)](https://console.x.ai/team/default/api-keys) | `XAI_API_KEY` | `xai/grok-4.5` |
| [QwenCloud Token Plan](https://home.qwencloud.com/api-keys) | `QWENCLOUD_API_KEY` | `qwencloud/qwen3.7-plus` |
Expand Down Expand Up @@ -264,9 +265,12 @@ from more than one provider before succeeding.
<details>
<summary><strong>Provider-specific setup</strong></summary>

- OpenAI uses your ChatGPT subscription rather than an API key. Connect from
- OpenAI / ChatGPT uses your ChatGPT subscription rather than an API key. Connect from
**Providers → OAuth providers → OpenAI / ChatGPT → Connect** in the Admin UI
and finish signing in through your browser. Restart an already-running agent after connecting.
- OpenAI API uses a separate Platform API key. Enter it under
**Providers → Cloud providers → OpenAI API → Configure**. The model list may
include IDs that cannot handle coding requests; choose a text-generation model.
- GitHub Copilot uses your signed-in GitHub account and subscription. Install
[Copilot CLI 1.0.83](https://github.com/github/copilot-cli/releases/tag/v1.0.83)
on PATH, then choose **Providers → OAuth providers → GitHub Copilot → Connect**.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "free-claude-code"
version = "6.2.58"
version = "6.2.59"
description = "Local proxy connecting coding agents to OpenAI-compatible AI providers"
readme = "README.md"
requires-python = ">=3.14.0"
Expand Down
4 changes: 4 additions & 0 deletions src/free_claude_code/config/admin/provider_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class ProviderFieldOverride(TypedDict, total=False):


_PROVIDER_FIELD_OVERRIDES: dict[str, ProviderFieldOverride] = {
"OPENAI_API_KEY": {
"label": "OpenAI API Key",
"description": "API key for the OpenAI Platform. Separate from ChatGPT sign-in.",
},
"OPENAI_PROXY": {
"description": (
"Optional proxy used for OpenAI sign-in and ChatGPT Codex requests. "
Expand Down
11 changes: 11 additions & 0 deletions src/free_claude_code/config/provider_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,17 @@ def configuration_attrs(self) -> tuple[str, ...]:
default_base_url=OPENAI_CODEX_DEFAULT_BASE,
proxy_attr="openai_proxy",
),
"openai_api": ProviderDescriptor(
provider_id="openai_api",
display_name="OpenAI API",
website_url="https://platform.openai.com/",
logo_filename="openai.svg",
credential_env="OPENAI_API_KEY",
credential_url="https://platform.openai.com/api-keys",
credential_attr="openai_api_key",
default_base_url="https://api.openai.com/v1",
proxy_attr="openai_api_proxy",
),
"github_copilot": ProviderDescriptor(
provider_id="github_copilot",
display_name="GitHub Copilot",
Expand Down
8 changes: 8 additions & 0 deletions src/free_claude_code/config/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,11 @@ class Settings(BaseModel):
extra="ignore",
)

# ==================== OpenAI Platform API ====================
openai_api_key: OptionalNonEmptyString = Field(
default=None, validation_alias="OPENAI_API_KEY"
)

# ==================== Azure OpenAI ====================
azure_openai_api_key: OptionalNonEmptyString = Field(
default=None, validation_alias="AZURE_OPENAI_API_KEY"
Expand Down Expand Up @@ -393,6 +398,9 @@ class Settings(BaseModel):
openai_proxy: OptionalNonEmptyString = Field(
default=None, validation_alias="OPENAI_PROXY"
)
openai_api_proxy: OptionalNonEmptyString = Field(
default=None, validation_alias="OPENAI_API_PROXY"
)
xai_proxy: OptionalNonEmptyString = Field(
default=None, validation_alias="XAI_PROXY"
)
Expand Down
5 changes: 5 additions & 0 deletions src/free_claude_code/providers/openai_api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""OpenAI Platform API-key provider."""

from .provider import OpenAIAPIProvider

__all__ = ["OpenAIAPIProvider"]
107 changes: 107 additions & 0 deletions src/free_claude_code/providers/openai_api/provider.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""OpenAI Platform provider using the shared Responses transport."""

from collections.abc import AsyncIterator, Mapping

import httpx2
from openai import AsyncOpenAI, DefaultAsyncHttpx2Client

from free_claude_code.application.model_metadata import ProviderModelInfo
from free_claude_code.core.anthropic.models import MessagesRequest
from free_claude_code.core.openai_responses import OpenAIResponsesRequest
from free_claude_code.core.reasoning import DEFAULT_REASONING_POLICY, ReasoningPolicy
from free_claude_code.providers.admission import (
ProviderAdmissionController,
ProviderOperationKind,
)
from free_claude_code.providers.base import BaseProvider, ProviderConfig
from free_claude_code.providers.model_listing import extract_openai_model_infos
from free_claude_code.providers.openai_responses import OpenAIResponsesTransport


class OpenAIAPIProvider(BaseProvider):
"""Own public API credentials and SDK resources, independent of ChatGPT OAuth."""

def __init__(
self,
config: ProviderConfig,
*,
admission: ProviderAdmissionController,
transport: httpx2.AsyncBaseTransport | None = None,
) -> None:
super().__init__(config)
if not config.api_key:
raise ValueError("OpenAI API requires an API key")
timeout = httpx2.Timeout(
config.http_read_timeout,
connect=config.http_connect_timeout,
write=config.http_write_timeout,
)
if transport is not None:
http_client = httpx2.AsyncClient(transport=transport, timeout=timeout)
elif config.proxy:
http_client = DefaultAsyncHttpx2Client(proxy=config.proxy, timeout=timeout)
else:
http_client = None
self._client = AsyncOpenAI(
api_key=config.api_key,
base_url=config.base_url,
max_retries=0,
timeout=timeout,
http_client=http_client,
)
self._admission = admission
self._responses = OpenAIResponsesTransport(
client=self._client,
admission=admission,
provider_name="OpenAI API",
read_timeout_s=config.http_read_timeout,
log_raw_sse_events=config.log_raw_sse_events,
)

async def cleanup(self) -> None:
await self._client.close()

async def list_model_infos(self) -> frozenset[ProviderModelInfo]:
payload = await self._admission.start_execution().run_call(
self._client.models.list,
operation_kind=ProviderOperationKind.MODEL_DISCOVERY,
)
return extract_openai_model_infos(payload, provider_name="OPENAI_API")

def stream_messages(
self,
request: MessagesRequest,
input_tokens: int = 0,
*,
request_id: str | None = None,
response_model: str | None = None,
reasoning: ReasoningPolicy = DEFAULT_REASONING_POLICY,
request_headers: Mapping[str, str] | None = None,
model_info: ProviderModelInfo | None = None,
) -> AsyncIterator[str]:
return self._responses.stream_messages(
request,
input_tokens=input_tokens,
request_id=request_id,
response_model=response_model or request.model,
reasoning=reasoning,
model_info=model_info,
)

def stream_responses(
self,
request: OpenAIResponsesRequest,
input_tokens: int = 0,
*,
request_id: str | None = None,
response_model: str | None = None,
reasoning: ReasoningPolicy = DEFAULT_REASONING_POLICY,
request_headers: Mapping[str, str] | None = None,
) -> AsyncIterator[str]:
return self._responses.stream_responses(
request,
input_tokens=input_tokens,
request_id=request_id,
response_model=response_model or request.model,
reasoning=reasoning,
)
14 changes: 14 additions & 0 deletions src/free_claude_code/providers/runtime/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ def construct(
return construct


def _load_openai_api() -> ProviderFactory:
from free_claude_code.providers.openai_api import OpenAIAPIProvider

def construct(
config: ProviderConfig,
settings: Settings,
admission: ProviderAdmissionController,
) -> BaseProvider:
return OpenAIAPIProvider(config, admission=admission)

return construct


def _load_mistral() -> ProviderFactory:
from free_claude_code.providers.mistral import MistralProvider

Expand Down Expand Up @@ -195,6 +208,7 @@ def construct(
_SPECIAL_PROVIDER_FACTORIES: dict[str, Callable[[], ProviderFactory]] = {
"nvidia_nim": _load_nvidia_nim,
"open_router": _load_open_router,
"openai_api": _load_openai_api,
"mistral": _load_mistral,
"kilo": _load_kilo,
"deepseek": _load_deepseek,
Expand Down
17 changes: 17 additions & 0 deletions tests/config/test_admin_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,23 @@ def test_connected_accounts_have_no_configuration_navigation_contract() -> None:
assert "missing_configuration_keys" not in status


def test_openai_api_key_card_is_independent_of_chatgpt_connection() -> None:
missing = _provider_status("openai_api", {})
configured = _provider_status(
"openai_api", {"OPENAI_API_KEY": _value("platform-key")}
)

assert missing["kind"] == "remote"
assert missing["status"] == "missing_key"
assert missing["configuration_keys"] == ["OPENAI_API_KEY"]
assert missing["settings_keys"] == ["OPENAI_API_KEY", "OPENAI_API_PROXY"]
assert configured["status"] == "configured"
assert (
_provider_status("openai", {"OPENAI_API_KEY": _value("platform-key")})["kind"]
== "connected_account"
)


def test_every_catalog_configuration_attribute_has_an_admin_field() -> None:
for descriptor in PROVIDER_CATALOG.values():
for settings_attr in descriptor.configuration_attrs():
Expand Down
13 changes: 13 additions & 0 deletions tests/config/test_provider_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,19 @@ def test_openai_is_a_connected_account_without_api_key_configuration() -> None:
assert descriptor.default_base_url == "https://chatgpt.com/backend-api/codex"


def test_openai_api_is_a_separate_key_configured_provider() -> None:
api = PROVIDER_CATALOG["openai_api"]
subscription = PROVIDER_CATALOG["openai"]

assert api.auth_kind is ProviderAuthKind.CONFIGURATION
assert api.credential_env == "OPENAI_API_KEY"
assert api.configuration_attrs() == ("openai_api_key",)
assert api.proxy_attr == "openai_api_proxy"
assert api.default_base_url == "https://api.openai.com/v1"
assert subscription.auth_kind is ProviderAuthKind.CONNECTED_ACCOUNT
assert subscription.credential_env is None


def test_copilot_is_connected_without_credential_configuration() -> None:
descriptor = PROVIDER_CATALOG["github_copilot"]
assert descriptor.auth_kind is ProviderAuthKind.CONNECTED_ACCOUNT
Expand Down
2 changes: 2 additions & 0 deletions tests/contracts/test_feature_manifest.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from free_claude_code.providers.mistral import MistralProvider
from free_claude_code.providers.nvidia_nim import NvidiaNimProvider
from free_claude_code.providers.open_router import OpenRouterProvider
from free_claude_code.providers.openai_api import OpenAIAPIProvider
from free_claude_code.providers.openai_chat import (
OPENAI_CHAT_PROFILES,
OpenAIChatProvider,
Expand Down Expand Up @@ -66,6 +67,7 @@ def test_product_coverage_is_not_satisfied_by_prereq_probes() -> None:
def test_provider_and_platform_registries_include_builtins() -> None:
specialized_provider_classes = {
"openai": OpenAICodexProvider,
"openai_api": OpenAIAPIProvider,
"github_copilot": GitHubCopilotProvider,
"opencode_zen": OpenCodeProvider,
"opencode_go": OpenCodeProvider,
Expand Down
1 change: 1 addition & 0 deletions tests/contracts/test_provider_catalog_order.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"groq",
"cline_pass",
"openai",
"openai_api",
"github_copilot",
"xai",
"qwencloud",
Expand Down
4 changes: 2 additions & 2 deletions tests/providers/test_credential_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,11 @@ def unexpected(request):
supported = {PROVIDER_CATALOG[row[0]].credential_env for row in CASES}
all_keys = {d.credential_env for d in PROVIDER_CATALOG.values() if d.credential_env}
keys = tuple(all_keys - supported)
assert len(keys) == 21
assert "OPENAI_API_KEY" in keys
result = await validation.check_credentials(
Settings.model_construct(), (*keys, "OPENCODE_API_KEY", "MODEL")
)
assert len(result) == 21
assert len(result) == len(keys)
assert all(
check.status == validation.CredentialStatus.UNVERIFIED for check in result
)
Expand Down
Loading
Loading