项目文件夹

文件
Ali Khokhar f8c21a48f2 Introduce a typed application boundary for provider execution (#1045)
## Problem

The HTTP adapter owned model routing, provider execution, and
runtime-facing contracts, so API handlers depended on provider
implementation types. Provider preflight also discovered private request
builders dynamically, obscuring the boundary that must fail before
streaming begins.

## Changes

| Before | After |
| --- | --- |
| `api/` owned model routing and shared provider execution. |
`application/` owns routing and a settings-independent
`ProviderExecutor`. |
| API handlers accepted `BaseProvider` callbacks. | API handlers consume
the narrow structural `ProviderPort`. |
| `BaseProvider` discovered `_build_request_body` dynamically. | Both
transport families implement explicit abstract preflight, with LM Studio
composing context validation. |
| Request leases, task control, and provider model metadata had
adapter/provider owners. | Application-owned ports and immutable values
define those cross-package contracts. |
| Boundary direction was implicit. | Architecture contracts and
documentation enforce the final dependency direction. |
| Package version was `3.4.19`. | Package version is `3.4.20`, with the
lockfile updated. |
| Coverage followed the old module layout. | Deterministic
boundary/preflight regressions and live Messages/Responses smokes cover
the new shape. |

<!-- greptile_comment -->

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

This PR adds a typed application boundary for provider execution. The
main changes are:

- New `application` package for routing, execution, ports, and model
metadata.
- API handlers now call application-owned routing and provider
execution.
- Provider preflight is now explicit on the transport families.
- Runtime API composition now uses a task-control port for `/stop`.
- Import-boundary tests, smoke references, docs, version, and lockfile
were updated.
</details>

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

This looks safe to merge.

No blocking issues found in the changed code.

None.

<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**
- Ran a deterministic pytest run for the provider boundary preflight,
which completed with exit code 0 and 102 tests passing in 4.77 seconds.
- Launched the environment presence check as part of the preflight,
which completed with exit code 0 and confirmed
OPENCODE\_API\_KEY=\[REDACTED\] matched.
- Attempted the live provider smoke test, which completed with exit code
0 and 2 tests skipped due to incomplete smoke configuration.

<a
href="https://app.greptile.com/trex/runs/14067905/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/application/execution.py | Moves shared provider
execution into the application layer and keeps eager preflight before
token counting and streaming. |
| src/free_claude_code/application/ports.py | Adds structural provider,
request-runtime, and task-control protocols used across the new
boundary. |
| src/free_claude_code/api/routes.py | Updates route composition to use
the application provider resolver and task-control stop path. |
| src/free_claude_code/providers/base.py | Makes provider preflight
explicit by requiring subclasses or transport bases to implement it. |
| src/free_claude_code/providers/transports/openai_chat/transport.py |
Adds OpenAI-chat preflight through the same request-body builder used by
streaming. |
|
src/free_claude_code/providers/transports/anthropic_messages/transport.py
| Adds native Messages preflight through the native request-body
builder. |
| src/free_claude_code/providers/model_listing.py | Keeps provider
model-list parsing while moving `ProviderModelInfo` ownership to the
application layer. |
| src/free_claude_code/runtime/bootstrap.py | Passes the runtime object
through the new `tasks` service slot. |
| tests/contracts/test_import_boundaries.py | Extends import-boundary
tests for the new application package. |

</details>

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

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

```mermaid
%%{init: {'theme': 'neutral'}}%%
flowchart LR
  API[api handlers and routes] --> Routing[application.routing]
  API --> Executor[application.execution]
  API --> Ports[application.ports]
  Executor --> ProviderPort[ProviderPort]
  ProviderPort --> Preflight[preflight_stream]
  ProviderPort --> Stream[stream_response]
  Runtime[runtime bootstrap and provider manager] --> Ports
  Providers[providers] --> Metadata[application.model_metadata]
  Executor --> Core[core anthropic and trace]
  Routing --> Config[config settings and model refs]
```

</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 handlers and routes] --> Routing[application.routing]
  API --> Executor[application.execution]
  API --> Ports[application.ports]
  Executor --> ProviderPort[ProviderPort]
  ProviderPort --> Preflight[preflight_stream]
  ProviderPort --> Stream[stream_response]
  Runtime[runtime bootstrap and provider manager] --> Ports
  Providers[providers] --> Metadata[application.model_metadata]
  Executor --> Core[core anthropic and trace]
  Routing --> Config[config settings and model refs]
```

</a>
</details>

<sub>Reviews (1): Last reviewed commit: ["Introduce typed application
boundary"](https://github.com/alishahryar1/free-claude-code/commit/4cdcf97231c812c6f568ca3d74af7ce759d7f2dc)
| [Re-trigger
Greptile](https://app.greptile.com/api/retrigger?id=43462788)</sub>

<!-- /greptile_comment -->
2026-07-10 20:06:57 -07:00

187 行
5.7 KiB
Python

from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from fastapi.testclient import TestClient
from free_claude_code.api.dependencies import get_settings
from free_claude_code.api.ports import ApiServices
from free_claude_code.application.ports import StopResult
from free_claude_code.config.settings import Settings
from tests.api.support import create_test_app
app = create_test_app()
@pytest.fixture
def client():
return TestClient(app)
@pytest.fixture
def mock_settings():
settings = Settings()
settings.fast_prefix_detection = True
settings.enable_network_probe_mock = True
settings.enable_title_generation_skip = True
return settings
def test_create_message_fast_prefix_detection(client, mock_settings):
app.dependency_overrides[get_settings] = lambda: mock_settings
payload = {
"model": "claude-3-sonnet",
"max_tokens": 100,
"messages": [{"role": "user", "content": "What is the prefix?"}],
}
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",
),
):
response = client.post("/v1/messages", json=payload)
assert response.status_code == 200
data = response.json()
assert "/ask" in data["content"][0]["text"]
app.dependency_overrides.clear()
def test_create_message_quota_check_mock(client, mock_settings):
app.dependency_overrides[get_settings] = lambda: mock_settings
payload = {
"model": "claude-3-sonnet",
"max_tokens": 100,
"messages": [{"role": "user", "content": "quota check"}],
}
with patch(
"free_claude_code.api.optimization_handlers.is_quota_check_request",
return_value=True,
):
response = client.post("/v1/messages", json=payload)
assert response.status_code == 200
assert "Quota check passed" in response.json()["content"][0]["text"]
app.dependency_overrides.clear()
def test_create_message_title_generation_skip(client, mock_settings):
app.dependency_overrides[get_settings] = lambda: mock_settings
payload = {
"model": "claude-3-sonnet",
"max_tokens": 100,
"messages": [{"role": "user", "content": "generate title"}],
}
with patch(
"free_claude_code.api.optimization_handlers.is_title_generation_request",
return_value=True,
):
response = client.post("/v1/messages", json=payload)
assert response.status_code == 200
assert "Conversation" in response.json()["content"][0]["text"]
app.dependency_overrides.clear()
def test_create_message_empty_messages_returns_400(client):
"""POST /v1/messages with messages: [] returns 400 invalid_request_error."""
payload = {
"model": "claude-3-sonnet",
"max_tokens": 100,
"messages": [],
}
response = client.post("/v1/messages", json=payload)
assert response.status_code == 400
data = response.json()
assert data.get("type") == "error"
assert data.get("error", {}).get("type") == "invalid_request_error"
assert "cannot be empty" in data.get("error", {}).get("message", "")
def test_count_tokens_empty_messages_returns_400(client):
"""POST /v1/messages/count_tokens with messages: [] matches messages validation."""
payload = {"model": "claude-3-sonnet", "messages": []}
response = client.post("/v1/messages/count_tokens", json=payload)
assert response.status_code == 400
data = response.json()
assert data.get("type") == "error"
assert data.get("error", {}).get("type") == "invalid_request_error"
assert "cannot be empty" in data.get("error", {}).get("message", "")
def test_count_tokens_endpoint(client):
payload = {
"model": "claude-3-sonnet",
"messages": [{"role": "user", "content": "hello"}],
}
with patch("free_claude_code.api.routes.get_token_count", return_value=5):
response = client.post("/v1/messages/count_tokens", json=payload)
assert response.status_code == 200
assert response.json()["input_tokens"] == 5
def test_count_tokens_error_returns_500(client):
"""When get_token_count raises, count_tokens returns 500."""
payload = {
"model": "claude-3-sonnet",
"messages": [{"role": "user", "content": "hello"}],
}
with patch(
"free_claude_code.api.routes.get_token_count",
side_effect=RuntimeError("token error"),
):
response = client.post("/v1/messages/count_tokens", json=payload)
assert response.status_code == 500
assert "token error" in response.json()["detail"]
def test_stop_cli_with_messaging_workflow(client):
session_control = MagicMock()
session_control.stop_all = AsyncMock(return_value=StopResult(cancelled_count=3))
services = app.state.services
app.state.services = ApiServices(
requests=services.requests,
admin=services.admin,
tasks=session_control,
)
response = client.post("/stop")
assert response.status_code == 200
assert response.json()["cancelled_count"] == 3
session_control.stop_all.assert_awaited_once()
def test_stop_cli_fallback_to_manager(client):
session_control = MagicMock()
session_control.stop_all = AsyncMock(return_value=StopResult(source="cli_manager"))
services = app.state.services
app.state.services = ApiServices(
requests=services.requests,
admin=services.admin,
tasks=session_control,
)
response = client.post("/stop")
assert response.status_code == 200
assert response.json()["source"] == "cli_manager"
session_control.stop_all.assert_awaited_once()