alishahryar1--free-claude-code
2a676cc6d9
## Problem Provider model discovery consumes metadata, but providers and the cache still expose a parallel IDs-only contract. The duplicate contract adds adapters and lets tests bypass capability metadata. ## Changes | Before | After | | --- | --- | | `BaseProvider` exposed `list_model_ids()` plus a metadata adapter. | `BaseProvider` exposes only abstract `list_model_infos()` returning application-owned metadata. | | Ordinary providers parsed IDs and converted them later. | Ordinary providers parse OpenAI-compatible catalogs directly into `ProviderModelInfo` values. | | OpenRouter, Cloudflare, and GitHub Models maintained redundant IDs-only wrappers. | Provider-specific filters and capability metadata have one return path. | | Vertex returned paginated IDs for the base adapter to wrap. | Vertex returns metadata after completing the same paginated discovery flow. | | The runtime cache exposed test-only raw-ID write and prefixed-ID read helpers. | The runtime cache accepts and returns metadata while retaining its production admin-status ID projection. | | Provider tests asserted the parallel IDs-only API. | Provider tests enforce the metadata-only contract and preserve provider-specific discovery behavior. | | The package version was `4.11.6`. | The package version is `4.11.7` with an updated lockfile. | <!-- greptile_comment --> <details open><summary><h3>Greptile Summary</h3></summary> This PR makes provider metadata the only model-catalog contract. The main changes are: - Makes `list_model_infos()` the abstract provider discovery API. - Migrates provider parsers and implementations to `ProviderModelInfo`. - Removes IDs-only cache and parser helpers. - Updates provider tests, architecture documentation, and package metadata. </details> <h3>Confidence Score: 4/5</h3> The catalog migration is consistent, but the release version must reflect the incompatible API removal. Repository provider and cache call paths use the new metadata shape consistently. Existing external consumers of the removed contracts can fail after a patch upgrade. The repository rules classify incompatible API removals as a major release. pyproject.toml and the matching package entry in uv.lock <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 external-consumer compatibility probe was run against both revisions, confirming the base still supports the legacy provider contract, while head fails with a TypeError due to the missing list\_model\_infos method, and runtime metadata shows head at version 4.11.7, indicating the removal is a patch transition rather than a major change. - An automated test suite completed successfully with 83 tests passing in 3.31 seconds. <a href="https://app.greptile.com/trex/runs/15207030/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/providers/base.py | Replaces the IDs-only provider API with an abstract metadata-only contract. | | src/free_claude_code/providers/model_listing.py | Consolidates OpenAI-compatible parsing into ProviderModelInfo results and removes IDs-only helpers. | | src/free_claude_code/providers/runtime/model_cache.py | Removes raw-ID helpers while retaining metadata storage and the admin ID projection. | | src/free_claude_code/providers/openai_chat/provider.py | Provides the metadata discovery implementation inherited by ordinary OpenAI-compatible providers. | | src/free_claude_code/providers/vertex/client.py | Preserves paginated discovery while returning metadata values. | | pyproject.toml | Uses a patch bump for a release that removes callable and importable contracts. | </details> <details open><summary><h3>Flowchart</h3></summary> <a href="#gh-light-mode-only"> ```mermaid %%{init: {'theme': 'neutral'}}%% flowchart LR A[Provider catalog endpoint] --> B[list_model_infos] B --> C[ProviderModelInfo set] C --> D[Provider model discovery] D --> E[ProviderModelCache] E --> F[Metadata-aware catalog] E --> G[Admin ID projection] ``` </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 A[Provider catalog endpoint] --> B[list_model_infos] B --> C[ProviderModelInfo set] C --> D[Provider model discovery] D --> E[ProviderModelCache] E --> F[Metadata-aware catalog] E --> G[Admin ID projection] ``` </a> </details> <a href="https://app.greptile.com/api/ide/codex?prompt=IMPORTANT%3A%20Work%20in%20the%20repository%20%22alishahryar1%2Ffree-claude-code%22%20on%20the%20existing%20branch%20%22ali%2Fprovider-model-info-contract%22.%20Checkout%20that%20branch%20%E2%80%94%20do%20NOT%20create%20a%20new%20branch%20or%20open%20a%20new%20PR.%20Push%20your%20changes%20to%20%22ali%2Fprovider-model-info-contract%22.%0A%0AFix%20the%20following%201%20code%20review%20issue.%20Work%20through%20them%20one%20at%20a%20time%2C%20proposing%20concise%20fixes.%0A%0A---%0A%0A%23%23%23%20Issue%201%20of%201%0Apyproject.toml%3A7%0A**Breaking%20Contract%20Ships%20as%20Patch**%0A%0AThis%20release%20removes%20%60BaseProvider.list_model_ids%28%29%60%20and%20cache%2Fparser%20methods%20that%20existing%20integrations%20can%20import%20or%20call.%20Such%20consumers%20will%20fail%20with%20%60TypeError%60%2C%20%60AttributeError%60%2C%20or%20%60ImportError%60%20after%20a%20patch%20upgrade%2C%20so%20this%20incompatible%20API%20change%20requires%20a%20major%20version%20bump%20under%20the%20repository's%20versioning%20rules.%0A%0A%60%60%60suggestion%0Aversion%20%3D%20%225.0.0%22%0A%60%60%60%0A%0A&repo=alishahryar1%2Ffree-claude-code&pr=1222&platform=github"><picture><source media="(prefers-color-scheme: dark)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodexDark.svg?v=6"><source media="(prefers-color-scheme: light)" srcset="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=6"><img alt="Fix All in Codex" src="https://greptile-static-assets.s3.amazonaws.com/badges/FixAllInCodex.svg?v=6"></picture></a> <sub>Reviews (1): Last reviewed commit: ["Make ProviderModelInfo the sole catalog ..."](https://github.com/alishahryar1/free-claude-code/commit/5c543eadc114201a4085d38885a27ac49153ca29) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=45869248)</sub> > Greptile also left **1 inline comment** on this PR. **Context used:** - Context used - CLAUDE.md ([source](https://app.greptile.com/alishahryar1/github/Alishahryar1/free-claude-code/-/custom-context?memory=d2fd24d8-0dec-4faf-8ee4-e085e215a2f8)) <!-- /greptile_comment -->
565 行
19 KiB
Python
565 行
19 KiB
Python
import json
|
|
from collections.abc import AsyncIterator
|
|
from typing import Any
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
from fastapi.responses import JSONResponse, StreamingResponse
|
|
|
|
from free_claude_code.api.handlers import (
|
|
MessagesHandler,
|
|
ResponsesHandler,
|
|
TokenCountHandler,
|
|
)
|
|
from free_claude_code.application.errors import InvalidRequestError
|
|
from free_claude_code.application.model_metadata import ProviderModelInfo
|
|
from free_claude_code.config.settings import Settings
|
|
from free_claude_code.core.anthropic.models import (
|
|
Message,
|
|
MessagesRequest,
|
|
TokenCountRequest,
|
|
)
|
|
from free_claude_code.core.anthropic.streaming import format_sse_event
|
|
from free_claude_code.core.failures import ExecutionFailure, FailureKind
|
|
from free_claude_code.core.openai_responses import OpenAIResponsesRequest
|
|
from free_claude_code.core.reasoning import ReasoningPolicy
|
|
|
|
_CLASSIFIER_SYSTEM = (
|
|
"You are a security monitor. Respond with <block>yes</block> or <block>no</block>."
|
|
)
|
|
_CLASSIFIER_USER = (
|
|
"<transcript>\nUser: review the repo\nWebFetch https://example.com: fetch\n"
|
|
"</transcript>\n<block> immediately."
|
|
)
|
|
|
|
|
|
class FakeProvider:
|
|
def __init__(self, events: list[str] | None = None) -> None:
|
|
self.preflight_calls: list[tuple[MessagesRequest, ReasoningPolicy]] = []
|
|
self.requests: list[MessagesRequest] = []
|
|
self.stream_kwargs: list[dict[str, Any]] = []
|
|
self.events = events or [
|
|
'event: message_start\ndata: {"type":"message_start"}\n\n',
|
|
'event: message_stop\ndata: {"type":"message_stop"}\n\n',
|
|
]
|
|
|
|
def preflight_stream(
|
|
self, request: MessagesRequest, *, reasoning: ReasoningPolicy
|
|
) -> None:
|
|
self.preflight_calls.append((request, reasoning))
|
|
|
|
async def cleanup(self) -> None:
|
|
return None
|
|
|
|
async def list_model_infos(self) -> frozenset[ProviderModelInfo]:
|
|
return frozenset({ProviderModelInfo("test-model")})
|
|
|
|
async def stream_response(
|
|
self,
|
|
request: MessagesRequest,
|
|
input_tokens: int = 0,
|
|
*,
|
|
request_id: str | None = None,
|
|
reasoning: ReasoningPolicy,
|
|
) -> AsyncIterator[str]:
|
|
self.requests.append(request)
|
|
self.stream_kwargs.append(
|
|
{
|
|
"input_tokens": input_tokens,
|
|
"request_id": request_id,
|
|
"reasoning": reasoning,
|
|
}
|
|
)
|
|
for event in self.events:
|
|
yield event
|
|
|
|
|
|
async def _streaming_body_text(response: StreamingResponse) -> str:
|
|
parts: list[str] = []
|
|
async for chunk in response.body_iterator:
|
|
if isinstance(chunk, bytes):
|
|
parts.append(chunk.decode("utf-8"))
|
|
else:
|
|
parts.append(str(chunk))
|
|
return "".join(parts)
|
|
|
|
|
|
def _json_response_content(response: JSONResponse) -> dict[str, Any]:
|
|
content = json.loads(bytes(response.body).decode("utf-8"))
|
|
assert isinstance(content, dict)
|
|
return content
|
|
|
|
|
|
def _trace_events(trace_mock: MagicMock, event: str) -> list[dict[str, Any]]:
|
|
return [
|
|
dict(call.kwargs)
|
|
for call in trace_mock.call_args_list
|
|
if call.kwargs.get("event") == event
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_messages_handler_passes_routed_request_and_stream_metadata() -> None:
|
|
provider = FakeProvider()
|
|
handler = MessagesHandler(Settings(), provider_resolver=lambda _: provider)
|
|
request = MessagesRequest(
|
|
model="nvidia_nim/test-model",
|
|
max_tokens=100,
|
|
stream=True,
|
|
messages=[Message(role="user", content="hi")],
|
|
)
|
|
|
|
response = await handler.create(request)
|
|
assert isinstance(response, StreamingResponse)
|
|
|
|
body = await _streaming_body_text(response)
|
|
assert "message_start" in body
|
|
assert provider.requests[0].model == "test-model"
|
|
assert provider.stream_kwargs[0]["input_tokens"] > 0
|
|
assert provider.stream_kwargs[0]["request_id"].startswith("req_")
|
|
assert provider.stream_kwargs[0]["reasoning"] == ReasoningPolicy.provider_default()
|
|
assert len(provider.preflight_calls) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("stream", [True, False])
|
|
async def test_messages_handler_preflight_invalid_request_stays_http_error(
|
|
stream: bool,
|
|
) -> None:
|
|
class RejectPreflightProvider(FakeProvider):
|
|
def preflight_stream(
|
|
self,
|
|
request: MessagesRequest,
|
|
*,
|
|
reasoning: ReasoningPolicy,
|
|
) -> None:
|
|
raise InvalidRequestError("bad tool shape")
|
|
|
|
provider = RejectPreflightProvider()
|
|
handler = MessagesHandler(Settings(), provider_resolver=lambda _: provider)
|
|
request = MessagesRequest(
|
|
model="nvidia_nim/test-model",
|
|
max_tokens=100,
|
|
messages=[Message(role="user", content="hi")],
|
|
stream=stream,
|
|
)
|
|
|
|
with pytest.raises(InvalidRequestError):
|
|
await handler.create(request)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_messages_handler_aggregates_provider_stream_when_stream_false() -> None:
|
|
provider = FakeProvider(
|
|
[
|
|
format_sse_event(
|
|
"message_start",
|
|
{
|
|
"type": "message_start",
|
|
"message": {
|
|
"id": "msg_test",
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [],
|
|
"model": "test-model",
|
|
"stop_reason": None,
|
|
"stop_sequence": None,
|
|
"usage": {"input_tokens": 7, "output_tokens": 1},
|
|
},
|
|
},
|
|
),
|
|
format_sse_event(
|
|
"content_block_start",
|
|
{
|
|
"type": "content_block_start",
|
|
"index": 0,
|
|
"content_block": {"type": "text", "text": ""},
|
|
},
|
|
),
|
|
format_sse_event(
|
|
"content_block_delta",
|
|
{
|
|
"type": "content_block_delta",
|
|
"index": 0,
|
|
"delta": {"type": "text_delta", "text": "OK"},
|
|
},
|
|
),
|
|
format_sse_event(
|
|
"content_block_stop", {"type": "content_block_stop", "index": 0}
|
|
),
|
|
format_sse_event(
|
|
"message_delta",
|
|
{
|
|
"type": "message_delta",
|
|
"delta": {"stop_reason": "end_turn", "stop_sequence": None},
|
|
"usage": {"input_tokens": 7, "output_tokens": 2},
|
|
},
|
|
),
|
|
format_sse_event("message_stop", {"type": "message_stop"}),
|
|
]
|
|
)
|
|
handler = MessagesHandler(Settings(), provider_resolver=lambda _: provider)
|
|
request = MessagesRequest(
|
|
model="nvidia_nim/test-model",
|
|
max_tokens=100,
|
|
stream=False,
|
|
messages=[Message(role="user", content="hi")],
|
|
)
|
|
|
|
response = await handler.create(request)
|
|
|
|
assert isinstance(response, JSONResponse)
|
|
assert response.headers["content-type"].startswith("application/json")
|
|
body = _json_response_content(response)
|
|
assert body["id"] == "msg_test"
|
|
assert body["type"] == "message"
|
|
assert body["role"] == "assistant"
|
|
assert body["model"] == "test-model"
|
|
assert body["content"] == [{"type": "text", "text": "OK"}]
|
|
assert body["stop_reason"] == "end_turn"
|
|
assert body["usage"] == {"input_tokens": 7, "output_tokens": 2}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_messages_handler_returns_error_json_for_stream_false_sse_error() -> None:
|
|
provider = FakeProvider(
|
|
[
|
|
format_sse_event(
|
|
"error",
|
|
{
|
|
"type": "error",
|
|
"error": {"type": "api_error", "message": "upstream failed"},
|
|
},
|
|
)
|
|
]
|
|
)
|
|
handler = MessagesHandler(Settings(), provider_resolver=lambda _: provider)
|
|
request = MessagesRequest(
|
|
model="nvidia_nim/test-model",
|
|
max_tokens=100,
|
|
stream=False,
|
|
messages=[Message(role="user", content="hi")],
|
|
)
|
|
|
|
response = await handler.create(request)
|
|
|
|
assert isinstance(response, JSONResponse)
|
|
assert response.status_code == 500
|
|
assert response.headers["x-should-retry"] == "false"
|
|
body = _json_response_content(response)
|
|
assert body["type"] == "error"
|
|
assert body["error"] == {"type": "api_error", "message": "upstream failed"}
|
|
assert body["request_id"].startswith("req_")
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_messages_handler_discards_partial_stream_false_output_on_error() -> None:
|
|
provider = FakeProvider(
|
|
[
|
|
format_sse_event(
|
|
"message_start",
|
|
{
|
|
"type": "message_start",
|
|
"message": {
|
|
"id": "msg_partial",
|
|
"type": "message",
|
|
"role": "assistant",
|
|
"content": [],
|
|
"model": "test-model",
|
|
"stop_reason": None,
|
|
"stop_sequence": None,
|
|
"usage": {"input_tokens": 1, "output_tokens": 1},
|
|
},
|
|
},
|
|
),
|
|
format_sse_event(
|
|
"content_block_start",
|
|
{
|
|
"type": "content_block_start",
|
|
"index": 0,
|
|
"content_block": {"type": "text", "text": ""},
|
|
},
|
|
),
|
|
format_sse_event(
|
|
"content_block_delta",
|
|
{
|
|
"type": "content_block_delta",
|
|
"index": 0,
|
|
"delta": {"type": "text_delta", "text": "incomplete"},
|
|
},
|
|
),
|
|
format_sse_event(
|
|
"error",
|
|
{
|
|
"type": "error",
|
|
"error": {
|
|
"type": "overloaded_error",
|
|
"message": "provider overloaded",
|
|
},
|
|
},
|
|
),
|
|
]
|
|
)
|
|
handler = MessagesHandler(Settings(), provider_resolver=lambda _: provider)
|
|
request = MessagesRequest(
|
|
model="nvidia_nim/test-model",
|
|
max_tokens=100,
|
|
stream=False,
|
|
messages=[Message(role="user", content="hi")],
|
|
)
|
|
|
|
response = await handler.create(request)
|
|
|
|
assert isinstance(response, JSONResponse)
|
|
assert response.status_code == 529
|
|
assert response.headers["x-should-retry"] == "false"
|
|
body = _json_response_content(response)
|
|
assert body["error"] == {
|
|
"type": "overloaded_error",
|
|
"message": "provider overloaded",
|
|
}
|
|
assert "content" not in body
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_messages_handler_stream_false_provider_exception_keeps_status() -> None:
|
|
class FailingProvider(FakeProvider):
|
|
async def stream_response(
|
|
self,
|
|
request: Any,
|
|
input_tokens: int = 0,
|
|
*,
|
|
request_id: str | None = None,
|
|
reasoning: ReasoningPolicy,
|
|
) -> AsyncIterator[str]:
|
|
self.requests.append(request)
|
|
self.stream_kwargs.append(
|
|
{
|
|
"input_tokens": input_tokens,
|
|
"request_id": request_id,
|
|
"reasoning": reasoning,
|
|
}
|
|
)
|
|
raise ExecutionFailure(
|
|
kind=FailureKind.RATE_LIMIT,
|
|
status_code=429,
|
|
message="upstream is busy",
|
|
retryable=True,
|
|
)
|
|
yield "unreachable"
|
|
|
|
provider = FailingProvider()
|
|
handler = MessagesHandler(Settings(), provider_resolver=lambda _: provider)
|
|
request = MessagesRequest(
|
|
model="nvidia_nim/test-model",
|
|
max_tokens=100,
|
|
stream=False,
|
|
messages=[Message(role="user", content="hi")],
|
|
)
|
|
|
|
response = await handler.create(request)
|
|
|
|
assert isinstance(response, JSONResponse)
|
|
assert response.status_code == 429
|
|
assert response.headers["x-should-retry"] == "false"
|
|
body = _json_response_content(response)
|
|
assert body["error"] == {
|
|
"type": "rate_limit_error",
|
|
"message": "upstream is busy",
|
|
}
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_messages_handler_forces_no_thinking_for_safety_classifier() -> None:
|
|
provider = FakeProvider()
|
|
handler = MessagesHandler(Settings(), provider_resolver=lambda _: provider)
|
|
request = MessagesRequest(
|
|
model="nvidia_nim/test-model",
|
|
max_tokens=100,
|
|
stream=True,
|
|
system=_CLASSIFIER_SYSTEM,
|
|
messages=[Message(role="user", content=_CLASSIFIER_USER)],
|
|
)
|
|
|
|
with patch("free_claude_code.api.handlers.messages.trace_event") as trace_mock:
|
|
response = await handler.create(request)
|
|
assert isinstance(response, StreamingResponse)
|
|
await _streaming_body_text(response)
|
|
|
|
assert provider.preflight_calls[0][1] == ReasoningPolicy.off()
|
|
assert provider.stream_kwargs[0]["reasoning"] == ReasoningPolicy.off()
|
|
assert provider.requests[0].model == "test-model"
|
|
assert provider.requests[0].system == _CLASSIFIER_SYSTEM
|
|
assert _trace_events(
|
|
trace_mock, "free_claude_code.api.optimization.safety_classifier_no_thinking"
|
|
) == [
|
|
{
|
|
"stage": "routing",
|
|
"event": "free_claude_code.api.optimization.safety_classifier_no_thinking",
|
|
"source": "api",
|
|
"model": "test-model",
|
|
"changed": True,
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_messages_handler_preserves_thinking_for_non_classifier() -> None:
|
|
provider = FakeProvider()
|
|
handler = MessagesHandler(Settings(), provider_resolver=lambda _: provider)
|
|
request = MessagesRequest(
|
|
model="nvidia_nim/test-model",
|
|
max_tokens=100,
|
|
stream=True,
|
|
system="Explain XML formats.",
|
|
messages=[
|
|
Message(
|
|
role="user",
|
|
content=(
|
|
"Explain <transcript>...</transcript> and a <block> tag "
|
|
"without making a verdict."
|
|
),
|
|
)
|
|
],
|
|
)
|
|
|
|
with patch("free_claude_code.api.handlers.messages.trace_event") as trace_mock:
|
|
response = await handler.create(request)
|
|
assert isinstance(response, StreamingResponse)
|
|
await _streaming_body_text(response)
|
|
|
|
assert provider.preflight_calls[0][1] == ReasoningPolicy.provider_default()
|
|
assert provider.stream_kwargs[0]["reasoning"] == ReasoningPolicy.provider_default()
|
|
assert (
|
|
_trace_events(
|
|
trace_mock,
|
|
"free_claude_code.api.optimization.safety_classifier_no_thinking",
|
|
)
|
|
== []
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_messages_handler_keeps_existing_no_thinking_for_classifier() -> None:
|
|
provider = FakeProvider()
|
|
handler = MessagesHandler(Settings(), provider_resolver=lambda _: provider)
|
|
request = MessagesRequest(
|
|
model="claude-3-freecc-no-thinking/nvidia_nim/test-model",
|
|
max_tokens=100,
|
|
stream=True,
|
|
system=_CLASSIFIER_SYSTEM,
|
|
messages=[Message(role="user", content=_CLASSIFIER_USER)],
|
|
)
|
|
|
|
with patch("free_claude_code.api.handlers.messages.trace_event") as trace_mock:
|
|
response = await handler.create(request)
|
|
assert isinstance(response, StreamingResponse)
|
|
await _streaming_body_text(response)
|
|
|
|
assert provider.preflight_calls[0][1] == ReasoningPolicy.off()
|
|
assert provider.stream_kwargs[0]["reasoning"] == ReasoningPolicy.off()
|
|
assert _trace_events(
|
|
trace_mock, "free_claude_code.api.optimization.safety_classifier_no_thinking"
|
|
) == [
|
|
{
|
|
"stage": "routing",
|
|
"event": "free_claude_code.api.optimization.safety_classifier_no_thinking",
|
|
"source": "api",
|
|
"model": "test-model",
|
|
"changed": False,
|
|
}
|
|
]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_messages_handler_optimization_intercepts_before_provider_execution() -> (
|
|
None
|
|
):
|
|
provider_resolver = MagicMock()
|
|
handler = MessagesHandler(Settings(), provider_resolver=provider_resolver)
|
|
request = MessagesRequest(
|
|
model="nvidia_nim/test-model",
|
|
max_tokens=100,
|
|
messages=[Message(role="user", content="quota check")],
|
|
)
|
|
optimized = object()
|
|
|
|
with patch(
|
|
"free_claude_code.api.handlers.messages.try_optimizations",
|
|
return_value=optimized,
|
|
):
|
|
assert await handler.create(request) is optimized
|
|
|
|
provider_resolver.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_responses_handler_bypasses_message_only_optimizations() -> None:
|
|
provider = FakeProvider()
|
|
handler = ResponsesHandler(Settings(), provider_resolver=lambda _: provider)
|
|
|
|
with patch(
|
|
"free_claude_code.api.handlers.messages.try_optimizations",
|
|
side_effect=AssertionError("Responses must not use message optimizations"),
|
|
):
|
|
response = await handler.create(
|
|
OpenAIResponsesRequest(
|
|
model="nvidia_nim/test-model",
|
|
input="quota check",
|
|
)
|
|
)
|
|
|
|
assert isinstance(response, StreamingResponse)
|
|
body = await _streaming_body_text(response)
|
|
assert "response.completed" in body
|
|
assert provider.requests[0].messages[0].content == "quota check"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_responses_handler_does_not_apply_safety_classifier_policy() -> None:
|
|
provider = FakeProvider()
|
|
handler = ResponsesHandler(Settings(), provider_resolver=lambda _: provider)
|
|
|
|
with patch("free_claude_code.api.handlers.messages.trace_event") as trace_mock:
|
|
response = await handler.create(
|
|
OpenAIResponsesRequest(
|
|
model="nvidia_nim/test-model",
|
|
input=_CLASSIFIER_USER,
|
|
instructions=_CLASSIFIER_SYSTEM,
|
|
)
|
|
)
|
|
|
|
assert isinstance(response, StreamingResponse)
|
|
await _streaming_body_text(response)
|
|
|
|
assert provider.preflight_calls[0][1] == ReasoningPolicy.provider_default()
|
|
assert provider.stream_kwargs[0]["reasoning"] == ReasoningPolicy.provider_default()
|
|
assert (
|
|
_trace_events(
|
|
trace_mock,
|
|
"free_claude_code.api.optimization.safety_classifier_no_thinking",
|
|
)
|
|
== []
|
|
)
|
|
|
|
|
|
def test_token_count_handler_routes_and_counts_tokens() -> None:
|
|
handler = TokenCountHandler(
|
|
Settings(),
|
|
token_counter=lambda messages, system, tools: len(messages) + 41,
|
|
)
|
|
|
|
with patch("free_claude_code.api.handlers.token_count.trace_event") as trace:
|
|
response = handler.count(
|
|
TokenCountRequest(
|
|
model="nvidia_nim/test-model",
|
|
messages=[Message(role="user", content="hi")],
|
|
),
|
|
request_id="req_ingress",
|
|
)
|
|
|
|
assert response.input_tokens == 42
|
|
assert all(
|
|
call.kwargs["request_id"] == "req_ingress" for call in trace.call_args_list
|
|
)
|