File size: 2,808 Bytes
0b2c941
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
"""Which wire dialect is this request written in?

Four dialects reach the intercept, because coding agents did not agree on one:

    openai_chat        opencode, qwen-coder, goose, swe-agent, mini-swe-agent, terminus-2, ...
    openai_responses   codex, trae-agent
    anthropic          claude-code
    google             gemini-cli, antigravity-sdk

Detection is by path first, then headers, then body shape — strongest signal to weakest. Path is
unambiguous when present; a header is a deliberate client declaration; body shape is a last resort
and can be coincidental, so it is only consulted when nothing better exists.

Getting this wrong is not subtle: a Google request parsed as chat-completions produces a 400 and the
agent silently does nothing, which reads as "captured nothing" rather than as a routing bug. That is
why trae-agent looked like a chat harness for a full night — its config says `provider: openai`, but
the access log showed exactly one `POST /v1/responses` against 465 chat calls.
"""

from __future__ import annotations

from enum import Enum
from typing import Any


class APIType(str, Enum):
    ANTHROPIC = "anthropic"
    OPENAI_CHAT = "openai_chat"
    OPENAI_RESPONSES = "openai_responses"
    GOOGLE = "google"


def detect(path: str, headers: dict[str, str], body: dict[str, Any]) -> APIType:
    """Classify one request. Defaults to chat-completions, the most common dialect."""
    if "/v1/messages" in path:
        return APIType.ANTHROPIC
    if "/v1/chat/completions" in path:
        return APIType.OPENAI_CHAT
    if "/v1/responses" in path:
        return APIType.OPENAI_RESPONSES
    # Google puts the method in the path: `/v1beta/models/{model}:generateContent`, and its
    # streaming variant `:streamGenerateContent`. The comparison must be case-insensitive: the
    # streaming form capitalises the G, so a literal `"generateContent" in path` matches the
    # non-streaming route and misses every streaming one. A missed Google request is then handed to
    # the chat-completions transformer, which finds no `messages` and produces a valid-looking
    # response in the wrong envelope, and gemini-cli reports that as nothing at all.
    if "generatecontent" in path.lower():
        return APIType.GOOGLE

    if "anthropic-version" in {k.lower() for k in headers}:
        return APIType.ANTHROPIC

    if "contents" in body:
        return APIType.GOOGLE
    if "input" in body and "instructions" in body:
        return APIType.OPENAI_RESPONSES

    return APIType.OPENAI_CHAT


def extract_model(api_type: APIType, body: dict[str, Any]) -> str:
    """The model name the client asked for, whatever the dialect calls it."""
    if api_type is APIType.GOOGLE:
        return body.get("model", "gemini-pro")
    return body.get("model", "unknown")