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
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.59"
version = "6.2.60"
description = "Local proxy connecting coding agents to OpenAI-compatible AI providers"
readme = "README.md"
requires-python = ">=3.14.0"
Expand Down
47 changes: 45 additions & 2 deletions src/free_claude_code/providers/openai_api/provider.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
"""OpenAI Platform provider using the shared Responses transport."""

import re
from collections.abc import AsyncIterator, Mapping

import httpx2
from openai import AsyncOpenAI, DefaultAsyncHttpx2Client
from openai import APIError, APIStatusError, 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.json_types import JsonObject
from free_claude_code.core.openai_responses import (
OpenAIResponsesRequest,
ResponsesStreamFailure,
)
from free_claude_code.core.reasoning import DEFAULT_REASONING_POLICY, ReasoningPolicy
from free_claude_code.providers.admission import (
ProviderAdmissionController,
Expand All @@ -18,6 +23,43 @@
from free_claude_code.providers.openai_responses import OpenAIResponsesTransport


def _sampling_retry_body(
error: Exception, body: JsonObject, sent_body: JsonObject
) -> JsonObject | None:
"""Omit only a sampling field explicitly rejected on this outbound attempt."""
if not isinstance(error, APIError | ResponsesStreamFailure):
return None
if isinstance(error, APIStatusError) and error.status_code != 400:
return None
detail = error.body
if not isinstance(detail, Mapping):
return None
param = detail.get("param")
if not isinstance(param, str) or param not in {"temperature", "top_p"}:
return None
if param not in body or param not in sent_body or body[param] != sent_body[param]:
return None
code = detail.get("code")
if code == "unsupported_value":
# This code also covers ordinary invalid values. Only default-only
# model incompatibility authorizes dropping the caller's setting.
message = detail.get("message")
if (
not isinstance(message, str)
or re.fullmatch(
rf"Unsupported value: ['\"]{param}['\"] does not support .+ "
r"with this model\. Only the default \([^)]+\) value is supported\.",
message.strip(),
re.IGNORECASE,
)
is None
):
return None
elif code != "unsupported_parameter":
return None
return {key: value for key, value in body.items() if key != param}


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

Expand Down Expand Up @@ -56,6 +98,7 @@ def __init__(
provider_name="OpenAI API",
read_timeout_s=config.http_read_timeout,
log_raw_sse_events=config.log_raw_sse_events,
request_correction=_sampling_retry_body,
)

async def cleanup(self) -> None:
Expand Down
19 changes: 19 additions & 0 deletions src/free_claude_code/providers/openai_responses/transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,17 @@ def __init__(
log_raw_sse_events: bool,
endpoint_transport: httpx2.AsyncBaseTransport | None = None,
event_adapter_factory: Callable[[], ResponsesEventAdapter] | None = None,
request_correction: Callable[
[Exception, JsonObject, JsonObject], JsonObject | None
]
| None = None,
omitted_request_fields: frozenset[str] = frozenset(),
tool_policy: ResponsesToolPolicy = ResponsesToolPolicy(),
) -> None:
self._client = client
self._endpoint_transport = endpoint_transport
self._event_adapter_factory = event_adapter_factory
self._request_correction = request_correction
self._omitted_request_fields = omitted_request_fields
self._tool_policy = tool_policy
self._admission = admission
Expand Down Expand Up @@ -449,6 +454,9 @@ async def _run_execution(
body,
sent_body=sent_body,
reasoning_error=raw_error,
after_common=partial(
self._provider_retry_body, raw_error, body, sent_body
),
),
)
if corrected_body is not None:
Expand Down Expand Up @@ -523,6 +531,17 @@ async def _run_execution(
raise execution.last_failure
raise RuntimeError("Responses execution ended without a terminal result.")

def _provider_retry_body(
self,
error: Exception,
body: JsonObject,
sent_body: JsonObject,
_used_retry_kinds: set[str],
) -> JsonObject | None:
if self._request_correction is None:
return None
return self._request_correction(error, body, sent_body)

async def _create_sdk_stream(
self,
body: JsonObject,
Expand Down
8 changes: 6 additions & 2 deletions tests/providers/test_openai_api_provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,14 @@ def _complete_stream(text: str) -> str:
)


def _provider(handler: httpx2.MockTransport) -> OpenAIAPIProvider:
def _provider(
handler: httpx2.MockTransport, *, max_attempts: int = 1
) -> OpenAIAPIProvider:
return OpenAIAPIProvider(
make_provider_config("platform-key", "https://api.openai.com/v1"),
admission=immediate_admission(provider_name="openai_api", max_attempts=1),
admission=immediate_admission(
provider_name="openai_api", max_attempts=max_attempts
),
transport=handler,
)

Expand Down
Loading
Loading