From faeec6bae8595170e8d82e996ac577c8c9c52ef1 Mon Sep 17 00:00:00 2001 From: Ali Khokhar Date: Thu, 24 Sep 2026 04:11:03 -0700 Subject: [PATCH] Add OpenAI Platform API-key provider (#1882) ## Why FCC's existing OpenAI provider uses ChatGPT OAuth and the Codex subscription endpoint, so an OpenAI Platform API key cannot be used as a direct provider. ## How Add a separate `openai_api` cloud provider with `OPENAI_API_KEY` and an optional dedicated proxy. It uses FCC's shared Responses transport for both client protocols and discovers models through the public `/v1/models` API. As agreed, the picker shows every returned ID with unknown capabilities; IDs unsuitable for coding may fail when selected. Update the README and bump the patch version to 6.2.59. --- README.md | 12 +- pyproject.toml | 2 +- .../config/admin/provider_manifest.py | 4 + .../config/provider_catalog.py | 11 + src/free_claude_code/config/settings.py | 8 + .../providers/openai_api/__init__.py | 5 + .../providers/openai_api/provider.py | 107 ++++++++ .../providers/runtime/factory.py | 14 ++ tests/config/test_admin_status.py | 17 ++ tests/config/test_provider_catalog.py | 13 + tests/contracts/test_feature_manifest.py | 2 + .../contracts/test_provider_catalog_order.py | 1 + tests/providers/test_credential_validation.py | 4 +- tests/providers/test_openai_api_provider.py | 236 ++++++++++++++++++ tests/providers/test_provider_runtime.py | 5 + uv.lock | 2 +- 16 files changed, 435 insertions(+), 8 deletions(-) create mode 100644 src/free_claude_code/providers/openai_api/__init__.py create mode 100644 src/free_claude_code/providers/openai_api/provider.py create mode 100644 tests/providers/test_openai_api_provider.py diff --git a/README.md b/README.md index d779e2da31..4362493131 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 `/` manually. 4. Click **Apply**. @@ -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/` | +| [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/` | | [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` | @@ -264,9 +265,12 @@ from more than one provider before succeeding.
Provider-specific setup -- 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**. diff --git a/pyproject.toml b/pyproject.toml index b56df96597..f4907bdaee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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" diff --git a/src/free_claude_code/config/admin/provider_manifest.py b/src/free_claude_code/config/admin/provider_manifest.py index c8133a640b..40259fb5d0 100644 --- a/src/free_claude_code/config/admin/provider_manifest.py +++ b/src/free_claude_code/config/admin/provider_manifest.py @@ -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. " diff --git a/src/free_claude_code/config/provider_catalog.py b/src/free_claude_code/config/provider_catalog.py index 65ae3342f4..9aa7677028 100644 --- a/src/free_claude_code/config/provider_catalog.py +++ b/src/free_claude_code/config/provider_catalog.py @@ -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", diff --git a/src/free_claude_code/config/settings.py b/src/free_claude_code/config/settings.py index bb694f3d6a..6f02c2d81a 100644 --- a/src/free_claude_code/config/settings.py +++ b/src/free_claude_code/config/settings.py @@ -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" @@ -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" ) diff --git a/src/free_claude_code/providers/openai_api/__init__.py b/src/free_claude_code/providers/openai_api/__init__.py new file mode 100644 index 0000000000..12f09ce8ae --- /dev/null +++ b/src/free_claude_code/providers/openai_api/__init__.py @@ -0,0 +1,5 @@ +"""OpenAI Platform API-key provider.""" + +from .provider import OpenAIAPIProvider + +__all__ = ["OpenAIAPIProvider"] diff --git a/src/free_claude_code/providers/openai_api/provider.py b/src/free_claude_code/providers/openai_api/provider.py new file mode 100644 index 0000000000..add4d23280 --- /dev/null +++ b/src/free_claude_code/providers/openai_api/provider.py @@ -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, + ) diff --git a/src/free_claude_code/providers/runtime/factory.py b/src/free_claude_code/providers/runtime/factory.py index 9a445daed0..87f5fd91e7 100644 --- a/src/free_claude_code/providers/runtime/factory.py +++ b/src/free_claude_code/providers/runtime/factory.py @@ -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 @@ -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, diff --git a/tests/config/test_admin_status.py b/tests/config/test_admin_status.py index 47b02899ad..be553e454d 100644 --- a/tests/config/test_admin_status.py +++ b/tests/config/test_admin_status.py @@ -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(): diff --git a/tests/config/test_provider_catalog.py b/tests/config/test_provider_catalog.py index 4b3d28bcde..b86282a345 100644 --- a/tests/config/test_provider_catalog.py +++ b/tests/config/test_provider_catalog.py @@ -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 diff --git a/tests/contracts/test_feature_manifest.py b/tests/contracts/test_feature_manifest.py index 1ae3c6413b..15b8122814 100644 --- a/tests/contracts/test_feature_manifest.py +++ b/tests/contracts/test_feature_manifest.py @@ -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, @@ -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, diff --git a/tests/contracts/test_provider_catalog_order.py b/tests/contracts/test_provider_catalog_order.py index ccd66bf13f..485fbb6fe8 100644 --- a/tests/contracts/test_provider_catalog_order.py +++ b/tests/contracts/test_provider_catalog_order.py @@ -11,6 +11,7 @@ "groq", "cline_pass", "openai", + "openai_api", "github_copilot", "xai", "qwencloud", diff --git a/tests/providers/test_credential_validation.py b/tests/providers/test_credential_validation.py index 356746b2e9..be92287d78 100644 --- a/tests/providers/test_credential_validation.py +++ b/tests/providers/test_credential_validation.py @@ -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 ) diff --git a/tests/providers/test_openai_api_provider.py b/tests/providers/test_openai_api_provider.py new file mode 100644 index 0000000000..5e9a21e735 --- /dev/null +++ b/tests/providers/test_openai_api_provider.py @@ -0,0 +1,236 @@ +"""Direct OpenAI Platform API provider wiring across both FCC request protocols.""" + +import json +from collections.abc import AsyncIterator +from typing import Any + +import httpx2 +import pytest + +from free_claude_code.application.model_metadata import ProviderModelInfo +from free_claude_code.core.anthropic.models import MessagesRequest +from free_claude_code.core.anthropic.stream_contracts import ( + parse_sse_text, + text_content, +) +from free_claude_code.core.openai_responses import OpenAIResponsesRequest +from free_claude_code.providers.openai_api.provider import OpenAIAPIProvider +from tests.providers.support import immediate_admission, make_provider_config + + +def _sse(*events: tuple[str, dict[str, Any]]) -> str: + return "".join( + f"event: {name}\ndata: {json.dumps(payload)}\n\n" for name, payload in events + ) + + +def _complete_stream(text: str) -> str: + return _sse( + ( + "response.created", + {"type": "response.created", "response": {"id": "resp_1"}}, + ), + ( + "response.output_text.delta", + {"type": "response.output_text.delta", "delta": text}, + ), + ( + "response.completed", + { + "type": "response.completed", + "response": { + "id": "resp_1", + "usage": {"input_tokens": 3, "output_tokens": 2}, + }, + }, + ), + ) + + +def _provider(handler: httpx2.MockTransport) -> OpenAIAPIProvider: + return OpenAIAPIProvider( + make_provider_config("platform-key", "https://api.openai.com/v1"), + admission=immediate_admission(provider_name="openai_api", max_attempts=1), + transport=handler, + ) + + +async def _collect(stream: AsyncIterator[str]) -> str: + return "".join([chunk async for chunk in stream]) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("ingress", ["messages", "responses"]) +async def test_api_key_provider_uses_public_responses_endpoint( + ingress: str, +) -> None: + requests: list[httpx2.Request] = [] + + def handler(request: httpx2.Request) -> httpx2.Response: + requests.append(request) + return httpx2.Response( + 200, + text=_complete_stream("hello"), + headers={"content-type": "text/event-stream"}, + request=request, + ) + + provider = _provider(httpx2.MockTransport(handler)) + try: + if ingress == "messages": + output = await _collect( + provider.stream_messages( + MessagesRequest.model_validate( + { + "model": "gpt-test", + "max_tokens": 64, + "messages": [{"role": "user", "content": "hello"}], + } + ) + ) + ) + assert text_content(parse_sse_text(output)) == "hello" + else: + output = await _collect( + provider.stream_responses( + OpenAIResponsesRequest.model_validate( + { + "model": "gpt-test", + "input": [{"role": "user", "content": "hello"}], + "max_output_tokens": 64, + "metadata": {"source": "api-key-test"}, + } + ) + ) + ) + assert "response.completed" in output + finally: + await provider.cleanup() + + assert len(requests) == 1 + request = requests[0] + assert str(request.url) == "https://api.openai.com/v1/responses" + assert request.headers["authorization"] == "Bearer platform-key" + assert "originator" not in request.headers + assert "chatgpt-account-id" not in request.headers + body = json.loads(request.content) + assert body["model"] == "gpt-test" + assert body["store"] is False + assert body["max_output_tokens"] == 64 + if ingress == "responses": + assert body["metadata"] == {"source": "api-key-test"} + + +@pytest.mark.asyncio +async def test_api_key_model_discovery_keeps_every_returned_id_unknown() -> None: + requests: list[httpx2.Request] = [] + + def handler(request: httpx2.Request) -> httpx2.Response: + requests.append(request) + return httpx2.Response( + 200, + json={ + "object": "list", + "data": [ + {"id": "gpt-test", "object": "model", "owned_by": "openai"}, + { + "id": "text-embedding-test", + "object": "model", + "owned_by": "openai", + }, + ], + }, + request=request, + ) + + provider = _provider(httpx2.MockTransport(handler)) + try: + models = await provider.list_model_infos() + finally: + await provider.cleanup() + + assert models == frozenset( + {ProviderModelInfo("gpt-test"), ProviderModelInfo("text-embedding-test")} + ) + assert str(requests[0].url) == "https://api.openai.com/v1/models" + assert requests[0].headers["authorization"] == "Bearer platform-key" + + +@pytest.mark.asyncio +async def test_api_key_provider_preserves_tool_calls() -> None: + tool_name = "mcp__example__search" + upstream_bodies: list[dict[str, Any]] = [] + + def handler(request: httpx2.Request) -> httpx2.Response: + body = json.loads(request.content) + upstream_bodies.append(body) + return httpx2.Response( + 200, + text=_sse( + ( + "response.output_item.added", + { + "type": "response.output_item.added", + "item": { + "type": "function_call", + "id": "fc_1", + "call_id": "call_1", + "name": tool_name, + }, + }, + ), + ( + "response.output_item.done", + { + "type": "response.output_item.done", + "item": { + "type": "function_call", + "id": "fc_1", + "call_id": "call_1", + "name": tool_name, + "arguments": "{}", + }, + }, + ), + ( + "response.completed", + { + "type": "response.completed", + "response": {"id": "resp_1"}, + }, + ), + ), + headers={"content-type": "text/event-stream"}, + request=request, + ) + + provider = _provider(httpx2.MockTransport(handler)) + try: + events = parse_sse_text( + await _collect( + provider.stream_messages( + MessagesRequest.model_validate( + { + "model": "gpt-test", + "messages": [{"role": "user", "content": "search"}], + "tools": [ + { + "name": tool_name, + "input_schema": {"type": "object"}, + } + ], + } + ) + ) + ) + ) + finally: + await provider.cleanup() + + assert upstream_bodies[0]["tools"][0]["name"] == tool_name + tool_start = next( + event.data["content_block"] + for event in events + if event.event == "content_block_start" + ) + assert tool_start["name"] == tool_name diff --git a/tests/providers/test_provider_runtime.py b/tests/providers/test_provider_runtime.py index 11db24d0a3..19ab851d35 100644 --- a/tests/providers/test_provider_runtime.py +++ b/tests/providers/test_provider_runtime.py @@ -55,6 +55,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, @@ -76,6 +77,7 @@ def _make_settings(**overrides): mock.model_opus = None mock.model_sonnet = None mock.model_haiku = None + mock.openai_api_key = "test_openai_api_key" mock.azure_openai_api_key = "test_azure_openai_key" mock.azure_openai_base_url = "https://test-resource.openai.azure.com/openai/v1/" mock.nvidia_nim_api_key = "test_key" @@ -170,6 +172,7 @@ def _make_settings(**overrides): mock.kilo_api_key = "test_kilo_key" mock.kilo_proxy = None mock.openai_proxy = None + mock.openai_api_proxy = None mock.xai_proxy = None mock.qwencloud_proxy = None mock.qwencloud_coding_proxy = None @@ -933,6 +936,7 @@ async def test_create_provider_instantiates_each_builtin(): cases = { "nvidia_nim": NvidiaNimProvider, "openai": OpenAICodexProvider, + "openai_api": OpenAIAPIProvider, "github_copilot": GitHubCopilotProvider, "cline_pass": OpenAIChatProvider, "xai": OpenAIChatProvider, @@ -1004,6 +1008,7 @@ async def test_create_provider_instantiates_each_builtin(): patch("free_claude_code.providers.openai_chat.client.AsyncOpenAI"), patch("free_claude_code.providers.github_copilot.provider.AsyncOpenAI"), patch("free_claude_code.providers.openai_codex.provider.AsyncOpenAI"), + patch("free_claude_code.providers.openai_api.provider.AsyncOpenAI"), patch("httpx.AsyncClient"), patch( "free_claude_code.providers.runtime.factory.ProviderAdmissionController", diff --git a/uv.lock b/uv.lock index 4b1fb145b5..ff4df5a676 100644 --- a/uv.lock +++ b/uv.lock @@ -599,7 +599,7 @@ wheels = [ [[package]] name = "free-claude-code" -version = "6.2.58" +version = "6.2.59" source = { editable = "." } dependencies = [ { name = "aiohttp" },