项目文件夹

文件
Ali Khokhar 4a0a0360de Move protocol models to their protocol owners (#1044)
## Problem

Anthropic Messages and OpenAI Responses wire models lived under the
inbound API adapter. Neutral protocol and provider code therefore
duck-typed requests, obscuring ownership and weakening dependency
boundaries.

## Changes

| Before | After |
| --- | --- |
| The API package owned Anthropic and Responses protocol models. | Each
protocol package owns and publicly exports its wire models. |
| Core and provider request paths accepted `Any` and probed known fields
with `getattr()`. | Core, transports, and providers consume concrete
`MessagesRequest` values. |
| Responses conversion and streaming received a dumped request mapping.
| Responses conversion and streaming receive one concrete
`OpenAIResponsesRequest`. |
| Anthropic request snapshots lived in generic tracing code. | Anthropic
request snapshots live with the protocol while generic tracing stays
protocol-independent. |
| Protocol tests and provider request doubles reflected the old API
ownership. | Protocol tests live under core and provider tests construct
real wire requests. |
| The API model package mixed protocol and model-catalog schemas. | The
API model package is removed, with catalog schemas beside catalog
construction and no compatibility shim. |
| Package version was `3.4.18`. | Package version is `3.4.19` with an
updated lockfile. |

<!-- greptile_comment -->

<details open><summary><h3>Greptile Summary</h3></summary>

This PR moves protocol request models to their protocol-owned packages.
The main changes are:

- Anthropic Messages models now live under `core.anthropic`.
- OpenAI Responses models now live under `core.openai_responses`.
- API handlers, routes, providers, and tests now use concrete protocol
request types.
- Anthropic request snapshots moved beside the Anthropic protocol
models.
- API model catalog schemas were kept with catalog response
construction.
- The package version and lockfile were updated.
</details>

<h3>Confidence Score: 5/5</h3>

This looks safe to merge.

No blocking issues were found in the changed code. Internal callers were
updated to pass the new concrete protocol models, and no stale internal
imports from the removed API model package were identified.

No files need attention.

<details><summary><h3><a href="https://www.greptile.com/trex"><img
alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="20" align="absmiddle"></a> T-Rex Logs</h3></summary>

**What T-Rex did**
- The Pytest run for protocol ownership focused tests completed, showing
71 passed in 6.24s and EXIT\_CODE: 0.
- A protocol import smoke script was generated for the
import/conversion/trace workflow.
- The protocol import smoke run completed successfully, including model
ownership output, adapter payload evidence, and trace snapshot evidence,
with EXIT\_CODE: 0.

<a
href="https://app.greptile.com/trex/runs/14066157/artifacts"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifactsDark.svg?v=4"><source
media="(prefers-color-scheme: light)"
srcset="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"><img
alt="View all artifacts"
src="https://greptile-static-assets.s3.amazonaws.com/badges/ViewAllArtifacts.svg?v=4"></picture></a>

<sub><a href="https://www.greptile.com/trex"><img alt="T-Rex"
src="https://greptile-static-assets.s3.amazonaws.com/trex/trex_green.svg"
height="14" align="absmiddle"></a> Ran code and verified through
T-Rex</sub>
</details>

<details open><summary><h3>Important Files Changed</h3></summary>

| Filename | Overview |
|----------|----------|
| src/free_claude_code/core/anthropic/models.py | Anthropic wire request
and response models moved under the Anthropic protocol package. |
| src/free_claude_code/core/anthropic/native_messages_request.py |
Native Anthropic serialization now expects concrete `MessagesRequest`
instances. |
| src/free_claude_code/core/anthropic/conversion.py | OpenAI chat
conversion now reads fields directly from `MessagesRequest`. |
| src/free_claude_code/core/anthropic/request_snapshot.py | Anthropic
request snapshotting moved from generic tracing into the protocol
package. |
| src/free_claude_code/core/openai_responses/models.py | OpenAI
Responses ingress models moved under the Responses protocol package. |
| src/free_claude_code/core/openai_responses/input.py | Responses
conversion now consumes the concrete request model instead of a dumped
mapping. |
| src/free_claude_code/core/openai_responses/streaming/assembler.py |
Responses stream assembly now reads request attributes from
`OpenAIResponsesRequest`. |
| src/free_claude_code/api/routes.py | Routes now import protocol
request models from their new core owners. |
| src/free_claude_code/api/model_catalog.py | Model-list response
schemas now live with model catalog construction. |

</details>

<details open><summary><h3>Flowchart</h3></summary>

<a href="#gh-light-mode-only">

```mermaid
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  API[API routes and handlers] --> Anthropic[core.anthropic models and helpers]
  API --> Responses[core.openai_responses models and adapter]
  Responses --> Anthropic
  Providers[Provider clients and transports] --> Anthropic
  Anthropic --> Trace[core.trace sanitization]
  API --> Catalog[api.model_catalog response schemas]
```

</a>
<a href="#gh-dark-mode-only">

```mermaid
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart LR
  API[API routes and handlers] --> Anthropic[core.anthropic models and helpers]
  API --> Responses[core.openai_responses models and adapter]
  Responses --> Anthropic
  Providers[Provider clients and transports] --> Anthropic
  Anthropic --> Trace[core.trace sanitization]
  API --> Catalog[api.model_catalog response schemas]
```

</a>
</details>

<sub>Reviews (1): Last reviewed commit: ["Move protocol models to their
protocol
o..."](https://github.com/alishahryar1/free-claude-code/commit/f1be5c1af4a10da710f80b5f9e7f6044a601f6af)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=43459428)</sub>

<!-- /greptile_comment -->
2026-07-10 19:26:27 -07:00

236 行
8.3 KiB
Python

"""Tests for api/optimization_handlers.py."""
from unittest.mock import patch
from free_claude_code.api.optimization_handlers import (
try_filepath_mock,
try_optimizations,
try_prefix_detection,
try_quota_mock,
try_suggestion_skip,
try_title_skip,
)
from free_claude_code.config.settings import Settings
from free_claude_code.core.anthropic.models import (
ContentBlockText,
Message,
MessagesRequest,
)
def _make_request(
messages_content: str, max_tokens: int | None = None
) -> MessagesRequest:
"""Create a MessagesRequest with a single user message."""
return MessagesRequest(
model="claude-3-sonnet",
max_tokens=max_tokens if max_tokens is not None else 100,
messages=[Message(role="user", content=messages_content)],
)
class TestTryPrefixDetection:
def test_disabled_returns_none(self):
settings = Settings()
settings.fast_prefix_detection = False
req = _make_request("x")
with patch(
"free_claude_code.api.optimization_handlers.is_prefix_detection_request",
return_value=(True, "/ask"),
):
assert try_prefix_detection(req, settings) is None
def test_enabled_and_match_returns_response(self):
settings = Settings()
settings.fast_prefix_detection = True
req = _make_request("x")
with (
patch(
"free_claude_code.api.optimization_handlers.is_prefix_detection_request",
return_value=(True, "/ask"),
),
patch(
"free_claude_code.api.optimization_handlers.extract_command_prefix",
return_value="/ask",
),
patch(
"free_claude_code.api.optimization_handlers.logger.info"
) as mock_log_info,
):
result = try_prefix_detection(req, settings)
assert result is not None
block = result.content[0]
assert isinstance(block, ContentBlockText)
assert block.text == "/ask"
mock_log_info.assert_called_once_with(
"Optimization: Fast prefix detection request"
)
def test_enabled_but_no_match_returns_none(self):
settings = Settings()
settings.fast_prefix_detection = True
req = _make_request("x")
with patch(
"free_claude_code.api.optimization_handlers.is_prefix_detection_request",
return_value=(False, ""),
):
assert try_prefix_detection(req, settings) is None
class TestTryQuotaMock:
def test_disabled_returns_none(self):
settings = Settings()
settings.enable_network_probe_mock = False
req = _make_request("quota", max_tokens=1)
with patch(
"free_claude_code.api.optimization_handlers.is_quota_check_request",
return_value=True,
):
assert try_quota_mock(req, settings) is None
def test_enabled_and_match_returns_response(self):
settings = Settings()
settings.enable_network_probe_mock = True
req = _make_request("quota", max_tokens=1)
with patch(
"free_claude_code.api.optimization_handlers.is_quota_check_request",
return_value=True,
):
result = try_quota_mock(req, settings)
assert result is not None
block = result.content[0]
assert isinstance(block, ContentBlockText)
assert "Quota check passed" in block.text
class TestTryTitleSkip:
def test_disabled_returns_none(self):
settings = Settings()
settings.enable_title_generation_skip = False
req = _make_request("write a 5-10 word title")
with patch(
"free_claude_code.api.optimization_handlers.is_title_generation_request",
return_value=True,
):
assert try_title_skip(req, settings) is None
def test_enabled_and_match_returns_response(self):
settings = Settings()
settings.enable_title_generation_skip = True
req = _make_request("x")
with patch(
"free_claude_code.api.optimization_handlers.is_title_generation_request",
return_value=True,
):
result = try_title_skip(req, settings)
assert result is not None
block = result.content[0]
assert isinstance(block, ContentBlockText)
assert block.text == "Conversation"
class TestTrySuggestionSkip:
def test_disabled_returns_none(self):
settings = Settings()
settings.enable_suggestion_mode_skip = False
req = _make_request("[SUGGESTION MODE: x]")
with patch(
"free_claude_code.api.optimization_handlers.is_suggestion_mode_request",
return_value=True,
):
assert try_suggestion_skip(req, settings) is None
def test_enabled_and_match_returns_response(self):
settings = Settings()
settings.enable_suggestion_mode_skip = True
req = _make_request("x")
with patch(
"free_claude_code.api.optimization_handlers.is_suggestion_mode_request",
return_value=True,
):
result = try_suggestion_skip(req, settings)
assert result is not None
block = result.content[0]
assert isinstance(block, ContentBlockText)
assert block.text == ""
class TestTryFilepathMock:
def test_disabled_returns_none(self):
settings = Settings()
settings.enable_filepath_extraction_mock = False
req = _make_request("Command:\nls\nOutput:\nfilepaths")
with patch(
"free_claude_code.api.optimization_handlers.is_filepath_extraction_request",
return_value=(True, "ls", "out"),
):
assert try_filepath_mock(req, settings) is None
def test_enabled_and_match_returns_response(self):
settings = Settings()
settings.enable_filepath_extraction_mock = True
req = _make_request("x")
with (
patch(
"free_claude_code.api.optimization_handlers.is_filepath_extraction_request",
return_value=(True, "ls", "a.txt b.txt"),
),
patch(
"free_claude_code.api.optimization_handlers.extract_filepaths_from_command",
return_value="a.txt\nb.txt",
),
):
result = try_filepath_mock(req, settings)
assert result is not None
block = result.content[0]
assert isinstance(block, ContentBlockText)
assert block.text == "a.txt\nb.txt"
def test_extract_filepaths_empty_list_still_returns_response(self):
settings = Settings()
settings.enable_filepath_extraction_mock = True
req = _make_request("x")
with (
patch(
"free_claude_code.api.optimization_handlers.is_filepath_extraction_request",
return_value=(True, "ls", "out"),
),
patch(
"free_claude_code.api.optimization_handlers.extract_filepaths_from_command",
return_value="",
),
):
result = try_filepath_mock(req, settings)
assert result is not None
block = result.content[0]
assert isinstance(block, ContentBlockText)
assert block.text == ""
class TestTryOptimizations:
def test_first_match_wins(self):
"""Quota mock is first in OPTIMIZATION_HANDLERS; it should win over prefix."""
settings = Settings()
settings.enable_network_probe_mock = True
settings.fast_prefix_detection = True
req = _make_request("quota", max_tokens=1)
with patch(
"free_claude_code.api.optimization_handlers.is_quota_check_request",
return_value=True,
):
result = try_optimizations(req, settings)
assert result is not None
block = result.content[0]
assert isinstance(block, ContentBlockText)
assert "Quota check passed" in block.text
def test_no_match_returns_none(self):
settings = Settings()
settings.fast_prefix_detection = False
settings.enable_network_probe_mock = False
settings.enable_title_generation_skip = False
settings.enable_suggestion_mode_skip = False
settings.enable_filepath_extraction_mock = False
req = _make_request("random user message")
assert try_optimizations(req, settings) is None