Skip to content

Claude: support custom models in gateway discovery - #375

Open
andy-xu-db wants to merge 15 commits into
mainfrom
andy.xu/claude-gateway-model-aliases
Open

Claude: support custom models in gateway discovery#375
andy-xu-db wants to merge 15 commits into
mainfrom
andy.xu/claude-gateway-model-aliases

Conversation

@andy-xu-db

@andy-xu-db andy-xu-db commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

Claude Code filters gateway-discovered models by ID, which hides custom model-service FQNs that do not contain claude or anthropic.

This change updates the relayed gateway proxy to:

  • advertise those models with an anthropic-aigw- ID while preserving the original display_name
  • rewrite pagination cursors to the upstream IDs
  • rewrite selected model IDs back before forwarding Messages API requests
  • reverse only aliases observed in discovery responses, leaving unrelated prefixed IDs untouched

Testing

  • uv run ruff check .
  • uv run pytest tests/test_gateway_proxy.py (28 passed)
  • uv run pytest (1944 passed, 37 skipped, 1 environment-dependent failure: the Claude user-agent e2e timed out before reaching its local capture server)

🥞 Stacked PR

Use this link to review incremental changes.


@andy-xu-db
andy-xu-db force-pushed the andy.xu/claude-gateway-model-aliases branch from c731346 to 19590dc Compare August 25, 2026 14:40
@andy-xu-db
andy-xu-db changed the base branch from main to claude/gateway-model-discovery-auth August 25, 2026 14:40
@andy-xu-db
andy-xu-db force-pushed the andy.xu/claude-gateway-model-aliases branch from 110431c to 19590dc Compare August 25, 2026 15:09
@andy-xu-db
andy-xu-db requested a review from lilly-luo August 25, 2026 20:40
Comment thread src/ucode/gateway_proxy.py Outdated
return headers


class _ModelAliases:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_ModelAlias -> _AnthropicModelAliases

Comment thread src/ucode/gateway_proxy.py Outdated
if not isinstance(model, dict) or not isinstance(model.get("id"), str):
continue
model_id = model["id"]
if "claude" in model_id.lower() or "anthropic" in model_id.lower():

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is in the right check or is it startsWith?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, in is correct since claude code checks if "claude" or "anthropic" exist anywhere in the name.

Comment thread src/ucode/gateway_proxy.py Outdated

def rewrite_path(self, path: str) -> str:
parsed = urlsplit(path)
if parsed.path != "/v1/models":

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you make this anthropic/v1/models? we shouldnt be doing this for codex

Comment thread src/ucode/gateway_proxy.py Outdated
Comment on lines +313 to +314
transform_models=self.command == "GET"
and urlsplit(self.path).path == "/v1/models",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you split this into a variable instead?

should_transform_model_names = self.command == "GET" and urlsplit(self.path).path == "/v1/models"

Comment thread src/ucode/gateway_proxy.py Outdated
Comment on lines +343 to +344
transform_models=self.command == "GET"
and urlsplit(self.path).path == "/v1/models",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same here

Comment thread src/ucode/gateway_proxy.py Outdated
self,
resp: httpx.Response,
*,
transform_models: bool = False,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no default value, force call site to specify

Comment thread src/ucode/gateway_proxy.py Outdated
Comment on lines +389 to +391
if key.lower() not in _HOP_BY_HOP and not (
transform_models and key.lower() == "content-encoding"
):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you rename this into a clearer variable? like what does the below signify?

not (
                    transform_models and key.lower() == "content-encoding"
                )

Comment thread src/ucode/gateway_proxy.py Outdated
for chunk in resp.iter_raw():
response_chunks = (
[self.model_aliases.advertise_models(resp.read())]
if transform_models and 200 <= resp.status_code < 300

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make 200 and 300 constants

@andy-xu-db
andy-xu-db force-pushed the claude/gateway-model-discovery-auth branch from b21bcf3 to dabbd5a Compare August 25, 2026 21:35
@andy-xu-db
andy-xu-db force-pushed the andy.xu/claude-gateway-model-aliases branch from 19590dc to 86edc01 Compare August 25, 2026 21:35
Base automatically changed from claude/gateway-model-discovery-auth to main August 25, 2026 22:03
Comment thread src/ucode/agents/claude.py Outdated

@lilly-luo lilly-luo Aug 25, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit could you make 127.0.0.1 a const and put it in a generic file? i'll need to use it as well

Comment thread src/ucode/gateway_proxy.py Outdated
self._original_by_alias: dict[str, str] = {}
self._lock = threading.Lock()

def advertise_models(self, body: bytes) -> bytes:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

advertise_models are like the models the user sees right? maybe UserDisplayedModels

Comment on lines +403 to +405
drop_stale_content_encoding = (
transform_model_names and header_name == "content-encoding"
)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does this mean? why can't we have content-encoding?

@@ -395,11 +498,17 @@ def start_proxy(
# to the gateway instead of a fresh handshake per request. Don't follow
# redirects — a proxy relays 3xx verbatim.
client = httpx.Client(base_url=upstream_base, timeout=_UPSTREAM_TIMEOUT, follow_redirects=False)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmmm is upstream_base then changing? because it used to be /anthropic. will this break relayed auth?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ic, is that why you had /v1/models as the orig path to match one? maybe we should leave it as it was before then and just rename to anthropic_gateway_proxy since it looks like this thing is pretty anthropic specific

Comment on lines +298 to +299
body = self.anthropic_model_aliases.rewrite_body(self.path, body)
url = self.anthropic_model_aliases.rewrite_path(self.path).lstrip("/")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will url here be diff btw relayed auth and the models?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants