alishahryar1--free-claude-code
4a0a0360de
## 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 -->
131 行
4.3 KiB
Python
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>"
|