项目文件夹

文件
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

131 行
4.3 KiB
Python

from unittest.mock import MagicMock
import pytest
from free_claude_code.api.command_utils import extract_filepaths_from_command
from free_claude_code.api.detection import (
is_filepath_extraction_request,
is_suggestion_mode_request,
)
from free_claude_code.core.anthropic.models import Message, MessagesRequest
def _mk_req(messages, tools=None, system=None):
req = MagicMock(spec=MessagesRequest)
req.messages = messages
req.tools = tools
req.system = system
return req
def _mk_msg(role: str, content):
msg = MagicMock(spec=Message)
msg.role = role
msg.content = content
return msg
class TestSuggestionMode:
def test_detects_suggestion_mode_in_any_user_message(self):
req = _mk_req(
[
_mk_msg("assistant", "ignore"),
_mk_msg("user", "Hello\n[SUGGESTION MODE: on]\nworld"),
]
)
assert is_suggestion_mode_request(req) is True
def test_suggestion_mode_ignores_non_user_messages(self):
req = _mk_req([_mk_msg("assistant", "[SUGGESTION MODE: on]")])
assert is_suggestion_mode_request(req) is False
class TestFilepathExtractionDetection:
def test_rejects_when_tools_present(self):
msg = _mk_msg(
"user",
"Command: cat foo.txt\nOutput: hi\n\nPlease extract <filepaths>.",
)
req = _mk_req([msg], tools=[{"name": "search"}])
ok, cmd, out = is_filepath_extraction_request(req)
assert (ok, cmd, out) == (False, "", "")
def test_rejects_when_missing_output_marker(self):
msg = _mk_msg(
"user",
"Command: cat foo.txt\n(no output marker)\n<filepaths>",
)
req = _mk_req([msg], tools=None)
ok, cmd, out = is_filepath_extraction_request(req)
assert (ok, cmd, out) == (False, "", "")
def test_rejects_when_not_asking_for_filepaths(self):
msg = _mk_msg("user", "Command: cat foo.txt\nOutput: hi")
req = _mk_req([msg], tools=None)
ok, cmd, out = is_filepath_extraction_request(req)
assert (ok, cmd, out) == (False, "", "")
def test_detects_filepath_extraction_via_system_block(self):
"""Command: + Output: in user, no filepaths in user; system has extract instructions."""
msg = _mk_msg("user", "Command: ls\nOutput: avazu-ctr\nfree-claude-code")
req = _mk_req(
[msg],
tools=None,
system="Extract any file paths that this command reads or modifies.",
)
ok, cmd, out = is_filepath_extraction_request(req)
assert ok is True
assert cmd == "ls"
assert "avazu-ctr" in out
assert "free-claude-code" in out
def test_extracts_command_and_output_and_cleans_output(self):
msg = _mk_msg(
"user",
"Command: cat foo.txt\n"
"Output: line1\nline2\n\n"
"Please extract <filepaths>.\n"
"<next_section>ignore me</next_section>",
)
req = _mk_req([msg], tools=None)
ok, cmd, out = is_filepath_extraction_request(req)
assert ok is True
assert cmd == "cat foo.txt"
assert out == "line1\nline2"
class TestExtractFilepathsFromCommand:
@pytest.mark.parametrize(
"command,expected_paths",
[
("ls -la", []),
("dir .", []),
("cat foo.txt", ["foo.txt"]),
("cat -n foo.txt bar.md", ["foo.txt", "bar.md"]),
("type C:\\tmp\\a.txt", ["C:\\tmp\\a.txt"]),
("grep pattern file1.txt file2.txt", ["file1.txt", "file2.txt"]),
("grep -n pattern file.txt", ["file.txt"]),
("grep -e pattern file.txt", ["file.txt"]),
("unknowncmd arg1 arg2", []),
("", []),
],
ids=[
"listing_ls",
"listing_dir",
"read_cat",
"read_cat_flags",
"read_type_windows_path",
"grep_simple",
"grep_with_flag",
"grep_with_e",
"unknown",
"empty",
],
)
def test_extracts_expected_paths(self, command, expected_paths):
result = extract_filepaths_from_command(command, output="(ignored)")
for p in expected_paths:
assert p in result
if not expected_paths:
assert result.strip() == "<filepaths>\n</filepaths>"