Chat client, Router dispatch and TypeSafe adapter
# E12-chat-client
Revision: 413ff02d7d2fee8486a984ddeabc6b653f862557
File: backend/app/services/router_client.py
Full source SHA-256: b24a7bf3388bb83676c622d92a3f6b9567f9a45efb5ade70da6ead8296f471a7
Extract ranges: [(320, 354), (428, 438), (459, 485)]
```text
320: "prompt": prompt,
321: "stream": stream,
322: }
323: if context:
324: payload["context"] = context
325: if idempotency_key:
326: payload["idempotency_key"] = idempotency_key
327: if minimum_quality:
328: payload["minimum_quality"] = minimum_quality
329: if cost_ceiling_aud is not None:
330: payload["cost_ceiling_aud"] = cost_ceiling_aud
331: if latency_budget_ms is not None:
332: payload["latency_budget_ms"] = latency_budget_ms
333: if fallback_policy:
334: payload["fallback_policy"] = fallback_policy
335: if allow_paid_fallback:
336: payload["allow_paid_fallback"] = True
337: if max_fallback_attempts is not None:
338: payload["max_fallback_attempts"] = max_fallback_attempts
339: if messages_raw is not None:
340: payload["messages"] = messages_raw
341: else:
342: # Legacy path: let the Router reconstruct messages from system + prompt
343: if system_prompt:
344: payload["system"] = system_prompt
345: token_limit = max_output_tokens if max_output_tokens is not None else settings.router_max_output_tokens
346: if token_limit is not None:
347: payload["max_output_tokens"] = token_limit
348: if pinned_model:
349: payload["pinned_model"] = pinned_model
350: payload["fallback_policy"] = "none"
351: elif settings.router_default_model:
352: payload["pinned_model"] = settings.router_default_model
353: if tools:
354: payload["tools"] = tools
428: )
429: token = await _get_token()
430:
431: async def _do_post(token: str) -> tuple[Response, str]:
432: async with AsyncClient(base_url=settings.router_url, timeout=timeout_seconds) as client:
433: resp = await client.post(
434: "/v1/route",
435: json=payload,
436: headers={"Authorization": f"Bearer {token}", "Content-Type": "application/json"},
437: )
438: return resp, token
459: router_meta = data.get("metadata")
460: if not isinstance(router_meta, dict):
461: router_meta = {}
462:
463: result_text = str(data.get("result") or "")
464: reasoning_text = data.get("reasoning")
465: model = data.get("model_used") or data.get("model") or router_meta.get("model")
466: raw_usage = data.get("tokens") or data.get("usage") or router_meta.get("tokens") or router_meta.get("usage")
467: metadata = {
468: "result": result_text,
469: "trace_id": data.get("trace_id"),
470: "model": model,
471: "primary_model": data.get("primary_model"),
472: "pinned": data.get("pinned"),
473: "fallback_used": data.get("fallback_used"),
474: "provider": data.get("provider") or router_meta.get("provider"),
475: "usage": _usage_or_estimate(
476: raw_usage,
477: payload=payload,
478: result=result_text,
479: reasoning=str(reasoning_text) if reasoning_text else None,
480: model=model,
481: ),
482: "cost": data.get("cost_usd") or data.get("cost") or router_meta.get("cost_usd") or router_meta.get("cost"),
483: "tool_calls": data.get("tool_calls"),
484: "reasoning": reasoning_text,
485: "provider_state": data.get("provider_state"),
```
# E03-request-path
Revision: 4a5d40211c9b1c1317575aec5639223694748853
File: app/api/routes.py
Full source SHA-256: 5cbfa6b3066b88c2f599af455fed1cc471f351aa16ee0860194c6bc0af8ff29b
Extract ranges: [(1491, 1523), (1558, 1584), (1940, 1975), (2100, 2118), (2127, 2179)]
```text
1491: @router.post("/v1/route", tags=["routing"], response_model=RouteResponse)
1492: async def route_request(
1493: body: RouteRequest,
1494: request: Request,
1495: token: TokenPayload = Depends(require_valid_token), # noqa: B008
1496: _scope_ok: None = Depends(require_scope(ROUTER_INVOKE_SCOPE)), # noqa: B008
1497: ) -> RouteResponse | JSONResponse | StreamingResponse:
1498: # --- Caller identity (needed for error logging) ---
1499: agent = token.sub
1500: identity_map = getattr(request.app.state, "identity_map", None)
1501: display_name = identity_map.lookup(agent) if identity_map else None
1502: is_admin = ROUTER_ADMIN_SCOPE in token.scopes
1503:
1504: # --- Trace ID ---
1505: trace_id = body.context.trace_id or uuid4()
1506: session = conversation_session(token.iss, token.sub, body.context.conversation_id)
1507:
1508: # --- Resolve app state ---
1509: catalogue = getattr(request.app.state, "catalogue", None)
1510: if catalogue is None:
1511: _log_error_request(
1512: agent=agent,
1513: task_type=body.task_type,
1514: error_type="routing_error",
1515: error_message="Model catalogue not loaded",
1516: trace_id=trace_id,
1517: token_scope=token.scope,
1518: display_name=display_name,
1519: )
1520: return JSONResponse(
1521: status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
1522: content=_error_body("routing_error", "Model catalogue not loaded"),
1523: )
1558: status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
1559: content=_error_body(
1560: "fx_rate_unavailable",
1561: "AUD cost ceiling supplied but FX rate is unavailable",
1562: trace_id=trace_id,
1563: ),
1564: )
1565:
1566: if is_admin:
1567: effective_ceiling = None
1568:
1569: # --- Token estimation ---
1570: est_input = estimate_input_tokens(body.system, body.prompt)
1571:
1572: # --- Candidate filtering ---
1573: available = frozenset(providers.keys())
1574: filter_result = filter_candidates(
1575: catalogue,
1576: body.task_type,
1577: caller_scopes=token.scopes,
1578: minimum_quality=body.minimum_quality,
1579: effective_cost_ceiling_usd=effective_ceiling,
1580: latency_budget_ms=resolved.latency_budget_ms,
1581: estimated_input_tokens=est_input,
1582: max_output_tokens=resolved.max_output_tokens,
1583: is_admin=is_admin,
1584: available_providers=available,
1940: if isinstance(exc, ProviderRateLimitError):
1941: return "rate_limit"
1942: if isinstance(exc, ProviderQuotaExhaustedError):
1943: return "quota_exhausted"
1944: return "provider_error"
1945:
1946: while True:
1947: provider_name = current_model.provider
1948: provider = providers.get(provider_name)
1949:
1950: if provider is None:
1951: fb_reason = "provider_error"
1952: if not fallback_reason:
1953: fallback_reason = fb_reason
1954: last_error_type = "provider_error"
1955: last_error_message = f"Provider '{provider_name}' not configured"
1956: if is_pinned or fallback_attempts >= body.max_fallback_attempts:
1957: break
1958: fb_result = select_fallback(
1959: catalogue,
1960: current_model,
1961: body.task_type,
1962: caller_scopes=token.scopes,
1963: minimum_quality=body.minimum_quality,
1964: effective_cost_ceiling_usd=effective_ceiling,
1965: fallback_policy=body.fallback_policy,
1966: allow_paid_fallback=body.allow_paid_fallback,
1967: max_fallback_attempts=body.max_fallback_attempts - fallback_attempts,
1968: estimated_input_tokens=est_input,
1969: max_output_tokens=resolved.max_output_tokens,
1970: latency_budget_ms=resolved.latency_budget_ms,
1971: already_failed=already_failed,
1972: available_providers=available,
1973: )
1974: if fb_result.exhausted or fb_result.selected is None:
1975: break
2100: response_body = RouteResponse(
2101: result=provider_response.result,
2102: provider=provider_response.provider,
2103: model_used=provider_response.model_used,
2104: primary_model=primary_model.id,
2105: pinned=is_pinned,
2106: fallback_used=fallback_used,
2107: fallback_reason=fallback_reason,
2108: cost_usd=actual_cost_usd,
2109: cost_aud=actual_cost_aud,
2110: estimated_cost_usd=est_cost_usd,
2111: estimated_cost_aud=est_cost_aud,
2112: fx_rate_usd_aud=fx_rate_value,
2113: fx_rate_date=fx_rate_date,
2114: latency_ms=provider_response.latency_ms,
2115: tokens=token_usage,
2116: trace_id=trace_id,
2117: status=route_status,
2118: tool_calls=(
2127: log_builder = new_log(agent, body.task_type)
2128: log_builder.trace_id(trace_id)
2129: log_builder.scopes_held(token.scope)
2130: if display_name:
2131: log_builder.agent_display_name(display_name)
2132: if body.context.agent_asserted:
2133: log_builder.agent_asserted(body.context.agent_asserted)
2134: if body.context.workflow:
2135: log_builder.workflow(body.context.workflow)
2136: if body.idempotency_key:
2137: log_builder.idempotency_key(body.idempotency_key)
2138: log_builder.minimum_quality(body.minimum_quality)
2139: log_builder.fallback_policy(body.fallback_policy)
2140: log_builder.provider(provider_response.provider)
2141: log_builder.primary_provider(primary_model.provider)
2142: log_builder.model(provider_response.model_used)
2143: log_builder.primary_model(primary_model.id)
2144: log_builder.routing_mode("direct" if current_model.routing_mode == "direct" else "zen")
2145: if provider_response.input_tokens is not None:
2146: log_builder.input_tokens(provider_response.input_tokens)
2147: if provider_response.output_tokens is not None:
2148: log_builder.output_tokens(provider_response.output_tokens)
2149: if est_cost_usd is not None:
2150: log_builder.estimated_cost_usd(est_cost_usd)
2151: if est_cost_aud is not None:
2152: log_builder.estimated_cost_aud(est_cost_aud)
2153: if actual_cost_usd is not None:
2154: log_builder.cost_usd(actual_cost_usd)
2155: if actual_cost_aud is not None:
2156: log_builder.cost_aud(actual_cost_aud)
2157: log_builder.effective_cost_ceiling_usd(effective_ceiling)
2158: if resolved.cost_ceiling_currency is not None:
2159: log_builder.cost_ceiling_currency(resolved.cost_ceiling_currency)
2160: if fx_rate_value is not None:
2161: log_builder.fx_rate_usd_aud(fx_rate_value)
2162: if fx_rate_date is not None:
2163: log_builder.fx_rate_date(fx_rate_date)
2164: if fx_rate_holder is not None:
2165: log_builder.fx_rate_source(fx_rate_holder.source)
2166: log_builder.latency_ms(provider_response.latency_ms)
2167: log_builder.provider_latency_ms(provider_response.latency_ms)
2168: log_builder.status(route_status)
2169: if fallback_used:
2170: log_builder.fallback_used(True)
2171: if fallback_reason:
2172: log_builder.fallback_reason(cast(FallbackReason, fallback_reason))
2173: if fallback_attempts:
2174: log_builder.fallback_attempts(fallback_attempts)
2175: if constraint_violation:
2176: log_builder.constraint_violation(cast(ConstraintViolation, constraint_violation))
2177: log_builder.prompt_redacted(body.prompt)
2178: log_builder.created_at(datetime.now(UTC))
2179: log_builder.build()
```
# E08-jev-adapter
Revision: 4a5d40211c9b1c1317575aec5639223694748853
File: app/providers/typesafe.py
Full source SHA-256: c8570411929a891e59c593745c0517cfb788b81b6676ecea55921f42936832de
Extract ranges: [(1, 114)]
```text
1: """Chat adapter for TypeSafe Jev.
2:
3: Jev is an evaluation API, not a chat-completions API. This adapter sends the
4: user text as state, asks one yes/no question, and returns the real score.
5: """
6:
7: from __future__ import annotations
8:
9: import time
10: from collections.abc import AsyncIterator
11: from typing import Any
12:
13: import httpx
14:
15: from app.providers.base import (
16: BaseProvider,
17: ProviderError,
18: ProviderRateLimitError,
19: ProviderResponse,
20: ProviderTimeoutError,
21: ProviderUnavailableError,
22: StreamChunk,
23: )
24:
25: TYPESAFE_ENDPOINT = "https://api.typesafe.ai/v1/systemone"
26: UPSTREAM_MODEL = "jev-latest"
27:
28:
29: class TypeSafeChatProvider(BaseProvider):
30: def __init__(
31: self,
32: api_key: str,
33: *,
34: client: httpx.AsyncClient | None = None,
35: endpoint: str = TYPESAFE_ENDPOINT,
36: ) -> None:
37: super().__init__(api_key, timeout_ms=30000, max_retries=0)
38: self._endpoint = endpoint
39: self._client = client or httpx.AsyncClient(follow_redirects=False)
40: self._owns_client = client is None
41:
42: async def aclose(self) -> None:
43: if self._owns_client:
44: await self._client.aclose()
45:
46: async def dispatch(
47: self,
48: model: str,
49: system: str | None,
50: prompt: str,
51: max_tokens: int,
52: temperature: float | None,
53: tools: list[dict[str, Any]] | None = None,
54: tool_choice: str | dict[str, Any] | None = None,
55: messages: list[dict[str, Any]] | None = None,
56: provider_timeout_ms: int | None = None,
57: provider_state: dict[str, Any] | None = None,
58: ) -> ProviderResponse:
59: del model, max_tokens, temperature, tools, tool_choice, provider_state
60: started = time.monotonic()
61: state = _state_text(system, prompt, messages)
62: payload = {
63: "model": UPSTREAM_MODEL,
64: "state": state,
65: "questions": {
66: "request": {
67: "type": "noul",
68: "instructions": "Is the user asking to test or use Jev?",
69: }
70: },
71: }
72: try:
73: response = await self._client.post(
74: self._endpoint,
75: headers={
76: "Authorization": f"Bearer {self.api_key}",
77: "Content-Type": "application/json",
78: },
79: json=payload,
80: timeout=self._request_timeout(provider_timeout_ms),
81: )
82: except httpx.TimeoutException as exc:
83: raise ProviderTimeoutError("TypeSafe timed out") from exc
84: except httpx.TransportError as exc:
85: raise ProviderUnavailableError("TypeSafe is unavailable") from exc
86: if response.status_code == 429:
87: raise ProviderRateLimitError("TypeSafe rate limited the request")
88: if response.status_code >= 500:
89: raise ProviderUnavailableError("TypeSafe returned a server error")
90: if response.status_code >= 400:
91: raise ProviderError("TypeSafe rejected the request")
92: try:
93: body = response.json()
94: score = float(body["answers"]["request"]["noul"])
95: usage = body.get("usage") or {}
96: returned_model = str(body.get("model") or UPSTREAM_MODEL)
97: except (KeyError, TypeError, ValueError) as exc:
98: raise ProviderError("TypeSafe returned an unreadable result") from exc
99: input_tokens = usage.get("input_tokens")
100: output_tokens = usage.get("output_tokens")
101: text = (
102: f"Jev {returned_model} scored this message. "
103: f"Yes-probability that it asks to test or use Jev: {score:.2f}. "
104: f"Tokens: {input_tokens} in, {output_tokens} out."
105: )
106: return ProviderResponse(
107: result=text,
108: provider="typesafe",
109: model_used=returned_model,
110: input_tokens=input_tokens if isinstance(input_tokens, int) else None,
111: output_tokens=output_tokens if isinstance(output_tokens, int) else None,
112: latency_ms=int((time.monotonic() - started) * 1000),
113: )
114:
```
A retained first-party operator record dated 1 October 2026 (SHA-256 827cfe5717a6837d6364cc325dc8a579d423002ad38a4fc117fc00b4be25f7ae) records one successful POST /v1/decisions and GET /v1/decisions/{invocation_id}, both HTTP 200, with full typed result equality. Router revision matches the revision above. That demonstrates the separate deployed request/storage path at that date. It does not establish generic /v1/route provider substitution, routine Chat/Iris adoption, current service enablement, authority to send arbitrary business data or judgment accuracy. The decision service requires dedicated scopes and a subject/use-case/rubric/data-policy grant; consumer configuration and authority must be reviewed separately. Jev's accuracy is unmeasured and its judgments remain advisory. No live model request was made for this page revision.