Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

FROM python:3.12.14-slim-trixie@sha256:78387bc3881b8273120a12ebe6c1ab22b018ccc2c9adf565ae1ac9b536e184ea

ARG VERSION=0.1.0
ARG VERSION=0.1.1
ARG VCS_REF=unknown
ARG OPENAPI_SHA256
ARG PIP_INDEX_URL=https://pypi.org/simple
Expand Down
12 changes: 10 additions & 2 deletions openapi.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: gh-aw-router HTTP API
version: 0.1.0
version: 0.1.1
license:
name: MIT
identifier: MIT
Expand Down Expand Up @@ -81,6 +81,8 @@ paths:
Model choices follow the embedded classification_choices list, filtered
to exact offered models and efforts. The first returned choice is preferred.
Reasoning models require an explicit effort. No default effort is inferred.
If no offered choice is in the classifier cell, the response is 422 with
code no_route. Malformed requests are still rejected with invalid_request.
requestBody:
required: true
content:
Expand All @@ -103,7 +105,13 @@ paths:
"415":
$ref: "#/components/responses/UnsupportedMediaType"
"422":
$ref: "#/components/responses/InvalidRequest"
description: >-
Request schema violation (invalid_json), unsupported operation
(invalid_request), or no eligible classifier choice (no_route).
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"500":
$ref: "#/components/responses/InternalError"
"504":
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "gh-aw-router"
version = "0.1.0"
version = "0.1.1"
description = "Standalone classification planning and model routing service"
readme = "README.md"
license = "MIT"
Expand Down
2 changes: 1 addition & 1 deletion src/gh_aw_router/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""Standalone gh-aw-router classification planning and model routing."""

__version__ = "0.1.0"
__version__ = "0.1.1"
8 changes: 5 additions & 3 deletions src/gh_aw_router/classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
TaskType,
TextPart,
)
from gh_aw_router.routing import NoRouteError

MAX_PRIOR_TURNS: Final = 6
MAX_PRIOR_TURN_CHARS: Final = 400
Expand Down Expand Up @@ -108,8 +109,9 @@ def create_classification_plan(
"""Rank exact offered choices by the embedded classifier preference order.

Preserve caller identities and prefer the first matching table entry. Raise
ClassificationError for missing authored text, omitted reasoning effort, or
an empty intersection. No provider calls or default-effort inference occur.
ClassificationError for missing authored text or omitted reasoning effort,
and NoRouteError when no offered choice is in the classifier cell. No
provider calls or default-effort inference occur.
"""
authored = authored_messages(request.conversation)
if not authored:
Expand Down Expand Up @@ -139,7 +141,7 @@ def create_classification_plan(
)
)
if not ranked:
raise ClassificationError("none of the available models is in the classifier routing cell")
raise NoRouteError("none of the available models is in the classifier routing cell")
return ClassifyResponse(
system_prompt=SYSTEM_PROMPT,
prompt=build_classification_prompt(authored),
Expand Down
10 changes: 9 additions & 1 deletion tests/fixtures/routing-contract/classify.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"conversation": [{"role": "user", "parts": [{"text": "Proceed."}]}],
"models": []
},
"status": 422, "response": {"code": "invalid_request", "detail": "invalid request: none of the available models is in the classifier routing cell"}
"status": 422, "response": {"code": "no_route", "detail": "no route: none of the available models is in the classifier routing cell"}
},
{
"id": "classify-missing-effort", "method": "POST", "path": "/classify",
Expand All @@ -30,5 +30,13 @@
"models": [{"id": "reasoning", "model": "github-copilot/router-reasoning"}]
},
"status": 422, "response": {"code": "invalid_request", "detail": "invalid request: reasoning effort must be specified for 'github-copilot/router-reasoning'"}
},
{
"id": "classify-unrecognised-choices", "method": "POST", "path": "/classify",
"request": {
"conversation": [{"role": "user", "parts": [{"text": "Proceed."}]}],
"models": [{"id": "absent", "model": "github-copilot/router-absent"}]
},
"status": 422, "response": {"code": "no_route", "detail": "no route: none of the available models is in the classifier routing cell"}
}
]
2 changes: 1 addition & 1 deletion tests/fixtures/routing-contract/discovery.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
{
"id": "capabilities", "method": "GET", "path": "/capabilities", "status": 200,
"response": {
"name": "gh-aw-router", "version": "0.1.0",
"name": "gh-aw-router", "version": "0.1.1",
"routing_profiles": [
{"goal": "cost", "mode": "economy"},
{"goal": "cost", "mode": "balanced"},
Expand Down
3 changes: 2 additions & 1 deletion tests/test_classification.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
RoutingModeRecommendation,
TextPart,
)
from gh_aw_router.routing import NoRouteError


def test_shared_classify_request_matches_policy_order(
Expand Down Expand Up @@ -109,7 +110,7 @@ def test_classification_requires_an_offered_routing_identity() -> None:
models=(ModelChoice(id="other", model="provider/other"),),
)

with pytest.raises(ClassificationError, match="none of the available models"):
with pytest.raises(NoRouteError, match="none of the available models"):
create_classification_plan(request, (ModelArm(model="provider/preferred"),))


Expand Down
8 changes: 5 additions & 3 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,19 +149,21 @@ def test_validate_data_reports_the_loaded_table_summary() -> None:
]


@pytest.mark.parametrize("command", ["route", "classify"])
@pytest.mark.parametrize("unsupported", [False, True])
def test_no_route_has_a_distinct_exit_status(
planning_payload: Callable[[str], dict[str, Any]],
synthetic_table_path: Path,
unsupported: bool,
command: str,
) -> None:
request = planning_payload("route")
request = planning_payload(command)
request["models"] = [{"id": "unsupported", "model": "provider/unknown"}] if unsupported else []
stdout = io.StringIO()
stderr = io.StringIO()

status = run(
["--routing-tables", str(synthetic_table_path), "route"],
["--routing-tables", str(synthetic_table_path), command],
stdin=io.StringIO(json.dumps(request)),
stdout=stdout,
stderr=stderr,
Expand Down Expand Up @@ -224,7 +226,7 @@ def test_parser_exposes_package_version(capsys: pytest.CaptureFixture[str]) -> N
cli.build_parser().parse_args(["--version"])

assert error.value.code == 0
assert capsys.readouterr().out == "gh-aw-router 0.1.0\n"
assert capsys.readouterr().out == "gh-aw-router 0.1.1\n"


@pytest.mark.parametrize(
Expand Down
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading