Skip to content

feat(custom-model): endpoint type selector for OpenAI or Anthropic protocol - #756

Closed
opticon454 wants to merge 16 commits into
Ark0N:masterfrom
opticon454:feature/custom-endpoint-anthropic
Closed

opticon454 wants to merge 16 commits into
Ark0N:masterfrom
opticon454:feature/custom-endpoint-anthropic

Conversation

@opticon454

@opticon454 opticon454 commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Context

A saved custom endpoint can only be treated as OpenAI-compatible. Gateways such as Bifrost also expose the Anthropic Messages protocol, which Claude Code, OpenCode, pi and omp speak natively.

Split out of the custom-endpoint PR (#752) at the maintainer's request, because it has an open design question.

What changed

  • Optional endpointType (openai or anthropic) on a saved endpoint, with a dropdown in the add and edit form. Omitted means openai, so nothing saved earlier changes.
  • Only Claude Code can use the Anthropic type (its registry entry alone declares supportsAnthropic). Every other CLI refuses an Anthropic-type endpoint with one shared message, and drops out of the Run menu for it.
  • For Claude Code, the injected ANTHROPIC_BASE_URL is the gateway's /anthropic root (the SDK appends /v1/messages) instead of the raw saved base URL the OpenAI type uses. Model discovery still uses the gateway's OpenAI /v1/models.

Open questions

Both are open on the maintainer's side and the code does not treat either as settled:

  1. withAnthropicRootSuffix hardcodes an /anthropic path segment, which is Bifrost's routing layout and not part of the Anthropic protocol. The alternative is to treat the saved base URL as the Anthropic base and only normalise the /v1.
  2. With the scope narrowed to Claude Code, an OpenAI-type endpoint already points ANTHROPIC_BASE_URL at the raw saved base URL, so the only thing this option changes is appending /anthropic.

Verification

  • typecheck is clean.
  • Full npm test at 8366e928: 689 files passed, 14,133 tests passed, 12 skipped, 1 failed. The failure is test/frontend-public-tooling.test.ts (public asset check), which needs gitignored spreadsheet assets absent from my test checkout; it does not touch this change.
  • Checked against Bifrost with Claude Code.

@Ark0N

Ark0N commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Thank you @opticon454 for splitting this out of #752. This PR adds an endpoint type to saved custom model endpoints so Claude Code, OpenCode, pi and omp can talk to a gateway's Anthropic Messages route, while the CLIs that cannot speak it refuse it with a clear message.

The design question you raised (whether to hardcode the /anthropic path segment or treat the saved base URL as the Anthropic base and only normalise /v1) is still open on my side. I have not decided it yet, so please hold off on reworking the /anthropic path handling itself until it is.

One thing needs fixing whichever way that goes:

1. pi and omp get a doubled /v1 (src/custom-model-injection.ts:269)

renderConfigFile gives pi-models-json and omp-models-yml the withAnthropicV1Suffix base (<root>/anthropic/v1). pi's anthropic-messages api hands baseUrl straight to the official Anthropic SDK (new PiAnthropic({ baseURL: model.baseUrl }) in @earendil-works/pi-ai), and the SDK appends /v1/messages itself, the same as Claude Code does. I ran real pi 1.1.0 (@anthropic-ai/sdk 0.129.0) with the models.json this PR generates against a small request-logging HTTP server:

  • PR config (baseUrl: <root>/anthropic/v1): POST /anthropic/v1/v1/messages?beta=true
  • same file with baseUrl: <root>/anthropic: POST /anthropic/v1/messages?beta=true

The command was pi -p --no-session --model custom/qwen3 "say hi" with HOME pointing at a directory holding the generated .pi/agent/models.json. If your Bifrost check used a different pi version, or Bifrost routes the doubled segment anyway, that would explain the difference. Please give the two pi-ai templates the root form that Claude Code gets (withAnthropicRootSuffix today), keep the /v1 form for OpenCode only, update the tests at test/custom-model-injection.test.ts:307 and :315, and correct the withAnthropicV1Suffix doc comment. omp shares pi-ai's anthropic-messages code, so the same change applies there; a quick run against a real omp would confirm it.

Smaller items for the same revision:

  1. Run menu (src/web/public/session-ui.js:927): for an Anthropic-type endpoint the menu still lists Codex, Grok, Gemini, Copilot and DeepSeek, and each of those fails on click. Please publish an anthropic flag per CLI in window.__codemanCustomModelClis (src/web/server.ts:1937), computed from the injection builder rather than an id list, and skip those rows.
  2. Route test (src/web/routes/session-routes.ts:1628 and :4531): the new refusal message has no test. One app.inject case each in test/routes/session-custom-model.test.ts and test/routes/quick-start-custom-model.test.ts would cover it. The two copies of the message could share one helper, and "only speaks the OpenAI-compatible protocol" is not accurate for Gemini; something like "cannot use an Anthropic-type endpoint; pick an OpenAI-type endpoint for it" fits every refused CLI.
  3. Docs: endpointType is a new field on the public /api/v1/model-endpoints routes, so it belongs in docs/api-reference.md (around line 1207) and in the "Adding an endpoint" section of docs/custom-model-endpoints.md, and the Custom Model Endpoint Profiles section of docs/architecture-invariants.md should mention endpointType and the supportsAnthropic registry flag. The help text in src/web/public/index.html:2795 ("Anthropic uses /anthropic/v1") already differs from what Claude Code gets, so that sentence, and these docs, can be written once the URL question is settled.

CI is green, and typecheck, lint, format and the custom-model and CLI registry test files all pass locally. Once the URL question is answered, items 1 to 4 against that answer are what this needs before merge.

@opticon454

Copy link
Copy Markdown
Contributor Author

Pushed dd83f0d, thanks for running real pi. Your result is right: the Anthropic SDK appends /v1/messages itself.

Before the review items, one scope change from the owner: the Anthropic endpoint type is now Claude Code only. The endpoints this tool points at are local models, never Claude models, so OpenCode, pi and omp have no Anthropic path any more and always use the OpenAI /v1 route (as they did before this PR). The OpenAI type stays available to every CLI, Claude Code included. An Anthropic-type endpoint is refused for every other CLI. I did not touch the /anthropic root form Claude Code gets, so the path question you hold is unchanged, and I did not edit the "Anthropic uses /anthropic/v1" hint wording beyond saying it is Claude Code only.

  1. Because pi and omp no longer have an Anthropic path, the doubled /v1 cannot happen (the pi-ai templates always write openai-completions with the /v1 base again), and withAnthropicV1Suffix is gone with its tests and doc comment.
  2. The Run menu: window.__codemanCustomModelClis carries a per-CLI supportsAnthropic flag computed from the injection builder, not an id list, and the menu skips refused rows for an Anthropic-type endpoint. Tests added.
  3. The refusal wording is one shared helper (" cannot use an Anthropic-type endpoint; pick an OpenAI-type endpoint for it"), with an app.inject case each in session-custom-model and quick-start-custom-model.
  4. Docs: endpointType in docs/api-reference.md and the "Adding an endpoint" section of docs/custom-model-endpoints.md, and endpointType plus the claude-only supportsAnthropic flag in the Custom Model Endpoint Profiles section of docs/architecture-invariants.md.

Full npm test on the narrowed branch: one test failed (render-index-html, which pins the shape of the published CLI list and now sees the flag); fixed in the last commit, and the custom-model and render tests pass (232).

@Ark0N

Ark0N commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Thank you @opticon454 for the quick revision. This PR adds an endpointType (openai or anthropic) to saved custom model endpoints; after the latest commits an Anthropic-type endpoint points Claude Code's ANTHROPIC_BASE_URL at the gateway's /anthropic root, and every other CLI refuses it with one shared message and drops out of the Run menu.

Round one is covered: the doubled /v1 for pi and omp is gone along with their Anthropic path, the Run menu skips refused rows through a supportsAnthropic flag computed from the injection builder, both routes share one refusal message with an app.inject test each, and the docs describe the field. Typecheck, lint, format, the frontend syntax check and the custom-model and CLI registry test files all pass on my side.

Two points on this PR are open, and the code and docs should not record either as settled until they are:

  1. The /anthropic path form. Unchanged from my last comment: whether the injected base hardcodes Bifrost's /anthropic segment (withAnthropicRootSuffix, src/custom-model-injection.ts:125) or treats the saved base URL as the Anthropic base. Please keep holding off on reworking it.

  2. The Claude Code only scope. This narrowing is new in this revision, and it changes what the selector does. For Claude Code, an OpenAI-type endpoint already sets ANTHROPIC_BASE_URL to the raw saved base URL (src/custom-model-injection.ts:157 to :161, pinned by test/custom-model-injection.test.ts:255), so Claude Code already speaks the Anthropic Messages protocol to <base>/v1/messages under the "OpenAI (default)" label; that is how the existing "Claude Code (llama.cpp)" entries work. With the scope narrowed, the only thing the "Anthropic" option changes is appending /anthropic to that base. A server that serves /v1/messages at its root and is saved as Anthropic-type then gets /anthropic/v1/messages and stops working, even though "Anthropic" reads like the right choice for it, and the help text at src/web/public/index.html:2795 does not tell the user that. This bears directly on point 1.

One change applies whichever way these go: the code comments, docs and one test describe the Claude Code only rule as an "owner decision" (src/custom-model-injection.ts:150, src/custom-model-hosts.ts:25, src/config/cli-registry/types.ts:890, docs/architecture-invariants.md:633, docs/custom-model-endpoints.md:60, test/custom-model-injection.test.ts:267). In this repository that phrase records a decision by the project owner, and docs/architecture-invariants.md is read as binding, so please state the reason itself and drop the attribution. The last sentence of the new docs/architecture-invariants.md paragraph ("remains under discussion") should also go before merge, since that file describes the code as it is.

Smaller items for the same revision:

  • src/custom-model-injection.ts:218: the opencode-json case gained braces and a reformatted options object with no logic change; please revert it to keep the diff to the feature.
  • test/custom-model-injection.test.ts:276: the test asserting no npm override on the OpenCode config checks a field nothing in the final code sets; it can go.
  • The PR description still describes Anthropic support for OpenCode, pi and omp; please update it to match the branch.

Once both open points are settled, what remains is aligning the code, help text and docs with that answer; the rest of this revision is in good shape.

opticon454 added a commit to opticon454/Codeman that referenced this pull request Nov 2, 2026
…d trim the diff

Third review round on PR Ark0N#756 (mechanical items): replace every 'owner decision' attribution for the Claude Code only rule with the reason itself (local models never speak a Claude-specific protocol); drop the 'remains under discussion' sentence from architecture-invariants.md; revert the opencode-json case to its exact upstream shape; delete the no-npm-override test for a field nothing sets. Open questions (/anthropic path form, scope, help text) untouched.
@opticon454

Copy link
Copy Markdown
Contributor Author

Pushed d66f4a7, thanks.

The two open points stay open and the code does not record either as settled: I have not touched the /anthropic path handling, and the help text at index.html is unchanged. Your second point is a fair reading (an OpenAI-type endpoint already gives Claude Code the raw base URL, so this option only appends /anthropic); I will come back on whether this PR should stay, be relabelled for a gateway route, or be dropped.

The mechanical items are in: the "owner decision" wording is gone from the code, docs and test and the reason is stated instead (the endpoints serve local models, which never speak a Claude-specific protocol), the "remains under discussion" sentence is deleted, the opencode-json case is back to its master shape, the test for a field nothing sets is removed, and the PR description now describes the Claude Code only scope.

Custom-model, CLI registry and render tests pass (506), plus typecheck, lint and format.

@Ark0N

Ark0N commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Thank you @opticon454 for the quick turnaround on round two. This PR adds an endpointType (openai or anthropic) to saved custom model endpoints; with an Anthropic-type endpoint, Claude Code's ANTHROPIC_BASE_URL gets the gateway's /anthropic root, and every other CLI refuses it with one shared message and drops out of the Run menu.

Everything from my last comment is in: the "owner decision" wording is gone, the "remains under discussion" sentence is deleted, the opencode-json case matches master, the unused npm test is removed and the description matches the branch. Typecheck, lint, format, the frontend syntax check, the CLI catalogue check and the custom-model, CLI registry and render tests pass here.

Both design points are still open on my side, and the code and docs should keep not recording either as settled:

  1. Whether the injected base hardcodes Bifrost's /anthropic segment (withAnthropicRootSuffix, src/custom-model-injection.ts:125) or treats the saved base URL as the Anthropic base and only normalises /v1.
  2. Whether a Claude Code only endpoint type earns its place, since an OpenAI-type endpoint already points Claude Code's ANTHROPIC_BASE_URL at the raw saved base URL.

Two facts that bear on them: under the second form of point 1, the Anthropic option would give Claude Code the same ANTHROPIC_BASE_URL as the OpenAI option apart from stripping a trailing /v1, so the two points are close to one question. And because the type is set per endpoint while what differs is per CLI, a Bifrost gateway used by Claude Code and by another CLI has to be saved twice (OpenAI-type for the others, Anthropic-type for Claude Code). Please keep holding off on the /anthropic path handling and the help text at src/web/public/index.html:2795.

If the PR stays, these apply whichever way the points are answered:

  1. The stated reason for the scope is not accurate (docs/architecture-invariants.md:633, src/custom-model-injection.ts:151, src/custom-model-hosts.ts:25, src/config/cli-registry/types.ts:890, docs/custom-model-endpoints.md:61, test/custom-model-injection.test.ts:267). "The endpoints this tool configures serve local models, which never speak a Claude-specific protocol" does not hold: the Anthropic Messages protocol is not tied to Claude models (the existing Claude Code path against llama.cpp works because llama.cpp answers /v1/messages), and the endpoints are not only local (docs/wiki/Custom-Model-Endpoints.md covers cloud services and gateways, and Bifrost is one). Please state the scope as what the code does, for example "only claude's recipe declares supportsAnthropic", without that justification.
  2. DeepSeek is an env recipe, not configDir (src/config/cli-registry/types.ts:892, docs/architecture-invariants.md:633, src/custom-model-injection.ts:184). Its recipe is DEEPSEEK_BASE_URL with appendV1Suffix (src/config/cli-registry/stock.ts:1570), refused through the missing supportsAnthropic flag, and the new architecture-invariants paragraph contradicts line 630 of the same file. Please move it to the env list in all three places.
  3. Pin the server half of the flag (test/render-index-html.test.ts:307). That test renders the real stock registry; one loop asserting supportsAnthropic is true for claude and false for every other entry covers the false branch of the probe in src/web/server.ts:1944, which nothing pins today.
  4. Warn on recovery when the type no longer fits (src/web/server.ts:1468). If an admin switches an endpoint that a codex or pi session uses to Anthropic, _rebuildCustomModelEnv returns undefined with no log line on the next restart, while the "endpoint no longer exists" branch above it warns. A matching console.warn makes that state visible.
  5. Small ones: the comment at src/custom-model-injection.ts:152 ends mid-sentence (the "and every other env recipe" clause never finishes); the test comment at test/custom-model-injection.test.ts:268 ("which briefly had anthropic-capable recipe branches") describes an earlier revision of this PR; docs/api-reference.md:1215 has one line far past the paragraph's wrap width.

Once the help text is settled, the user manual needs the dropdown too: step 1 of "Adding an endpoint" in docs/wiki/Custom-Model-Endpoints.md:15, and the "OpenAI-compatible endpoints" note at docs/wiki/HTTP-API.md:183.

The two open points come first. Once they are answered, what remains is aligning the code, help text and docs with that answer, plus the items above.

@opticon454

Copy link
Copy Markdown
Contributor Author

Pushed 1214acb, thanks. The two design points stay open and I have not touched the /anthropic path handling or the help text; your point that the type is set per endpoint while what differs is per CLI is a good one, and I will come back on whether this PR should stay.

  1. The scope is now stated as what the code does ("only claude's recipe declares supportsAnthropic; every other CLI refuses an Anthropic-type endpoint"), with the justification removed, in the code comments, docs/architecture-invariants.md, docs/custom-model-endpoints.md and the test.
  2. DeepSeek is listed with the env recipes in all three places (it is DEEPSEEK_BASE_URL with appendV1Suffix, refused for the missing flag), so the new paragraph no longer contradicts line 630.
  3. test/render-index-html.test.ts renders the real stock registry and asserts supportsAnthropic is true for claude and false for every other entry.
  4. Recovery logs a console.warn when an endpoint's type no longer fits the CLI, like the "endpoint no longer exists" branch.
  5. The unfinished comment is finished, the stale test comment is fixed and the long line in docs/api-reference.md is wrapped.

The wiki dropdown docs wait on the help text, as you said. Full npm test: 13,518 passed, 0 failed.

@Ark0N

Ark0N commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Thank you @opticon454 for round three. This PR adds an endpointType (openai or anthropic) to saved custom model endpoints; with an Anthropic-type endpoint, Claude Code's ANTHROPIC_BASE_URL gets the gateway's /anthropic root, and every other CLI refuses it with one shared message and drops out of the Run menu.

Round two is in: the scope is stated as code facts, DeepSeek sits with the env recipes, test/render-index-html.test.ts pins supportsAnthropic for every stock CLI, recovery logs a warning, and the unfinished comment, the stale test comment and the long doc line are fixed. Typecheck, lint, format, the frontend syntax check, the CLI catalogue check and FULL_GATE_DRAFT pass here.

Both design points are still open on my side, unchanged from my last comment:

  1. Whether the injected base hardcodes Bifrost's /anthropic segment (withAnthropicRootSuffix, src/custom-model-injection.ts:125) or treats the saved base URL as the Anthropic base and only normalises /v1.
  2. Whether a Claude Code only endpoint type earns its place, since an OpenAI-type endpoint already points Claude Code's ANTHROPIC_BASE_URL at the raw saved base URL.

One more fact that bears on both. Discovery calls <saved baseUrl>/v1/models (src/web/routes/custom-model-routes.ts:309), and running the builder on four saved bases gives:

saved baseUrl discovery Claude Code, OpenAI type Claude Code, Anthropic type
http://gw:8080 /v1/models http://gw:8080 http://gw:8080/anthropic
http://gw:8080/anthropic /anthropic/v1/models http://gw:8080/anthropic http://gw:8080/anthropic
http://gw:8080/anthropic/v1 /anthropic/v1/v1/models http://gw:8080/anthropic/v1 http://gw:8080/anthropic

So the two types give Claude Code a different URL only when the saved base is the bare gateway root, and the /anthropic/v1 form that withAnthropicRootSuffix accepts (test/custom-model-injection.test.ts:59) cannot discover. It also means "Model discovery always uses the gateway's OpenAI /v1/models" (src/web/public/index.html:2795, docs/custom-model-endpoints.md:66, docs/api-reference.md:1215, docs/architecture-invariants.md:633, src/custom-model-hosts.ts:28) holds for a root base only. Please keep holding off on the path handling and the help text; that sentence is part of what gets written once point 1 is answered.

Small items for the same revision, whichever way the points go:

  1. src/custom-model-injection.ts:49: the endpointType doc comment on CustomModelEndpoint still gives the "local models are never Claude models" reason that round two asked to remove. Please state it as the code does, the way you did elsewhere (only claude's recipe declares supportsAnthropic; every other recipe returns { kind: 'unsupported' }).
  2. src/web/server.ts:1478: the recovery warning says the endpoint "is Anthropic-type" for every undefined from applyCustomModelInjection, but that also covers a CLI whose recipe is unsupported (after a clis.json override, for example), and this path has no kind === 'unsupported' check before it, unlike the two routes. Please pick the wording from endpoint.endpointType, the way customModelApplyErrorMessage (src/web/routes/session-routes.ts:805) does; moving that helper next to applyCustomModelInjection lets both sites share it.
  3. The PR description still gives the "serve local models, which never speak a Claude-specific protocol" reason; please update it to match the code comments.

The two open points come first. Once they are answered, what remains is aligning the code, help text, docs and the wiki pages (docs/wiki/Custom-Model-Endpoints.md:15, docs/wiki/HTTP-API.md:183) with that answer, plus the three items above.

opticon454 added a commit to opticon454/Codeman that referenced this pull request Nov 2, 2026
…thfully word the recovery warning

Review round on PR Ark0N#756, small items 1 and 2.

- The endpointType doc comment dropped the "local models are never Claude
  models" rationale: the code fact is that only claude's recipe declares
  supportsAnthropic and every other recipe returns unsupported for an
  Anthropic-type endpoint
- customModelApplyErrorMessage moved from session-routes.ts next to
  applyCustomModelInjection (its natural home) and exported, so the two
  route sites and the recovery warning share one wording
- The recovery warning no longer claims "is Anthropic-type" for every
  undefined from applyCustomModelInjection (an unsupported recipe, e.g.
  after a clis.json override, warned the same lie); it picks the sentence
  from endpoint.endpointType like the routes do
- New test/custom-model-recovery-warning.test.ts pins both shapes: an
  OpenAI-type endpoint with antigravity's unsupported recipe warns without
  "Anthropic-type"; an Anthropic-type endpoint warns with it
@opticon454

Copy link
Copy Markdown
Contributor Author

Pushed 2278764. The two design points stay open and I have not touched the path handling, the help text or the discovery sentences. The table was useful: it shows the two types differ for Claude Code only when the saved base is the bare gateway root, which fits your reading that the two points are close to one question. I will come back on whether this PR stays.

  1. The endpointType doc comment on CustomModelEndpoint now states what the code does (only claude's recipe declares supportsAnthropic; every other recipe returns { kind: 'unsupported' }), with the reason removed.
  2. customModelApplyErrorMessage moved next to applyCustomModelInjection and both routes and the recovery warning share it, so the wording follows endpoint.endpointType: an OpenAI-type endpoint whose CLI recipe is unsupported no longer says "Anthropic-type". Recovery test added for both cases.
  3. The PR description no longer gives the old reason.

Full npm test: 13,520 passed, 0 failed.

@Ark0N

Ark0N commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Thank you @opticon454 for round four. This PR adds an endpointType (openai or anthropic) to saved custom model endpoints; with an Anthropic-type endpoint, Claude Code's ANTHROPIC_BASE_URL gets the gateway's /anthropic root, and every other CLI refuses it with one shared message and drops out of the Run menu.

Round three is in: the endpointType doc comment on CustomModelEndpoint states the code fact, customModelApplyErrorMessage lives next to applyCustomModelInjection and is shared by both routes and the recovery warning (so an OpenAI-type endpoint on a CLI with no mechanism no longer logs "Anthropic-type"), test/custom-model-recovery-warning.test.ts pins both wordings, and the description matches the branch. Typecheck, lint, format, the frontend syntax check, the browser-exclude check, the CLI catalogue check, the custom-model and CLI registry test files and FULL_GATE_DRAFT pass here.

Both design points are still open on my side, unchanged:

  1. Whether the injected base hardcodes Bifrost's /anthropic segment (withAnthropicRootSuffix, src/custom-model-injection.ts:124) or treats the saved base URL as the Anthropic base and only normalises /v1.
  2. Whether a Claude Code only endpoint type earns its place, since an OpenAI-type endpoint already points Claude Code's ANTHROPIC_BASE_URL at the raw saved base URL, and the type is set per endpoint while what differs is per CLI.

Please keep holding off on the path handling, the help text at src/web/public/index.html:2795, the discovery sentence and the wiki pages. Nothing else is needed on the branch until these are answered. You offered your own read on whether this PR should stay, be relabelled for a gateway route, or be dropped; that read is welcome here.

If the PR stays, two small items apply whichever way the points are answered:

  1. Refuse the type mismatch before the llama-swap checks (src/web/routes/session-routes.ts:1575 and :4486). For a non-claude CLI and an Anthropic-type endpoint, both routes first probe GET /running and can answer requiresConfirmation: true (:1610, :4508), asking the caller to approve evicting another session's model, and only then refuse at the apply (:1619, :4520). Nothing is evicted, since triggerLlamaSwapLoad runs after the apply, but the caller answers a question about an apply that cannot happen. Checking buildCustomModelInjection(entry, endpoint, modelId).kind === 'unsupported' right after the endpoint lookup, next to the existing kind === 'unsupported' check (:1539, :4453), and returning customModelApplyErrorMessage there would refuse it first. The Run menu already hides these rows, so this is reachable through the API only.
  2. Stop the server in the new test (test/custom-model-recovery-warning.test.ts:44): it constructs a WebServer and never calls stop(), unlike every other test that constructs one. An await server.stop() in the existing afterAll keeps it in line.

The two open points come first. Once they are answered, what remains is aligning the code, help text, docs and the wiki pages (docs/wiki/Custom-Model-Endpoints.md:15, docs/wiki/HTTP-API.md:183) with that answer, plus the two items above.

opticon454 added a commit to opticon454/Codeman that referenced this pull request Nov 2, 2026
…a-swap checks

Review round on PR Ark0N#756, both small items.

- The swap route and the quick-start customModel path now run
  buildCustomModelInjection right after the endpoint lookup, next to the
  existing unsupported-recipe check, and return the shared
  customModelApplyErrorMessage when the kind is unsupported. A non-claude
  CLI pointing at an Anthropic-type endpoint never reaches the
  GET /running probe or a requiresConfirmation answer, so the caller is no
  longer asked to approve evicting another session's model for an apply
  that cannot happen (API-only path; the Run menu already hides the rows)
- Route tests in both files pair the refusal with a /running mock that
  WOULD ask for confirmation (ready model + conflicting session) and pin
  that /running was never fetched
- test/custom-model-recovery-warning.test.ts stops the WebServer it
  constructs, in line with every other test that builds one
@opticon454

Copy link
Copy Markdown
Contributor Author

Pushed e5fa234, thanks. Both design points stay as they were; I have not touched the path handling, the help text, the discovery sentence or the wiki pages, and I will answer your question about whether this PR stays separately.

  1. Both routes now refuse the type mismatch right after the endpoint lookup, before the GET /running probe: buildCustomModelInjection(...).kind === 'unsupported' returns customModelApplyErrorMessage, so a non-claude CLI with an Anthropic-type endpoint never gets a requiresConfirmation answer. A route test each (swap and quick-start) uses a llama-swap mock that would have asked for confirmation and asserts the refusal and that /running was never fetched.
  2. test/custom-model-recovery-warning.test.ts stops its WebServer in afterAll.

Full npm test: 13,522 passed, 0 failed.

@Ark0N

Ark0N commented Nov 2, 2026

Copy link
Copy Markdown
Owner

Thank you @opticon454 for round five. This PR adds an endpointType (openai or anthropic) to saved custom model endpoints; with an Anthropic-type endpoint, Claude Code's ANTHROPIC_BASE_URL gets the gateway's /anthropic root, and every other CLI refuses it with one shared message and drops out of the Run menu.

Round four is in:

  • The swap route and quick-start both refuse an Anthropic-type endpoint for a CLI that cannot use it right after the endpoint lookup (src/web/routes/session-routes.ts:1555 and :4474), before the llama-swap probe, and the two new route tests pin that /running is never fetched and no requiresConfirmation comes back.
  • test/custom-model-recovery-warning.test.ts stops its WebServer in afterAll.

Typecheck, lint, format, the frontend syntax check, the browser-exclude check, the CLI catalogue check, the custom-model, render and route test files and FULL_GATE_DRAFT pass here.

Both design points are still open on my side, unchanged:

  1. Whether the injected base hardcodes Bifrost's /anthropic segment (withAnthropicRootSuffix, src/custom-model-injection.ts:124) or treats the saved base URL as the Anthropic base and only normalises /v1.
  2. Whether a Claude Code only endpoint type earns its place, since an OpenAI-type endpoint already points Claude Code's ANTHROPIC_BASE_URL at the raw saved base URL, and the type is set per endpoint while what differs is per CLI.

Nothing else is needed on the branch until these are answered, so please keep holding off on the path handling, the help text at src/web/public/index.html:2795, the discovery sentence and the wiki pages. Your own read on whether this PR should stay, be relabelled for a gateway route, or be dropped is welcome here.

Once the two points are answered, what remains is aligning the code, help text, docs and the wiki pages (docs/wiki/Custom-Model-Endpoints.md:15, docs/wiki/HTTP-API.md:183) with that answer.

…sed run-menu rows, share the refusal wording, document endpointType

- renderConfigFile now hands the pi-ai lineage (pi, omp) the same
  withAnthropicRootSuffix base Claude Code gets; their SDK appends
  /v1/messages itself, so the previous /anthropic/v1 form doubled the
  /v1 (live-confirmed against real pi). OpenCode alone keeps
  withAnthropicV1Suffix; the doc comments are corrected to match.
- server.ts publishes supportsAnthropic per CLI on
  window.__codemanCustomModelClis, computed by running
  buildCustomModelInjection against a synthetic Anthropic probe
  endpoint (never an id list); session-ui.js skips those rows for an
  Anthropic-type endpoint.
- The duplicated apply-time refusal is one shared helper with the
  wording the review asked for, and both routes gained an app.inject
  refusal test (codex against an Anthropic-type endpoint).
- endpointType documented in api-reference POST/PUT,
  custom-model-endpoints Adding an endpoint, and
  architecture-invariants Custom Model Endpoint Profiles.

The /anthropic path-segment design question stays untouched.
…only

Owner decision: local models are never Claude models. OpenCode, pi and omp lose their anthropic recipe branches (no @ai-sdk/anthropic override, no anthropic-messages api, no /anthropic/v1 base helper); every configDir and configContentEnv recipe refuses an Anthropic-type endpoint, env recipes still gate on supportsAnthropic (claude's alone). withAnthropicRootSuffix and claude's behaviour are untouched. Run-menu flag and shared refusal message stay as they were.
…d trim the diff

Third review round on PR Ark0N#756 (mechanical items): replace every 'owner decision' attribution for the Claude Code only rule with the reason itself (local models never speak a Claude-specific protocol); drop the 'remains under discussion' sentence from architecture-invariants.md; revert the opencode-json case to its exact upstream shape; delete the no-npm-override test for a field nothing sets. Open questions (/anthropic path form, scope, help text) untouched.
…thfully word the recovery warning

Review round on PR Ark0N#756, small items 1 and 2.

- The endpointType doc comment dropped the "local models are never Claude
  models" rationale: the code fact is that only claude's recipe declares
  supportsAnthropic and every other recipe returns unsupported for an
  Anthropic-type endpoint
- customModelApplyErrorMessage moved from session-routes.ts next to
  applyCustomModelInjection (its natural home) and exported, so the two
  route sites and the recovery warning share one wording
- The recovery warning no longer claims "is Anthropic-type" for every
  undefined from applyCustomModelInjection (an unsupported recipe, e.g.
  after a clis.json override, warned the same lie); it picks the sentence
  from endpoint.endpointType like the routes do
- New test/custom-model-recovery-warning.test.ts pins both shapes: an
  OpenAI-type endpoint with antigravity's unsupported recipe warns without
  "Anthropic-type"; an Anthropic-type endpoint warns with it
…a-swap checks

Review round on PR Ark0N#756, both small items.

- The swap route and the quick-start customModel path now run
  buildCustomModelInjection right after the endpoint lookup, next to the
  existing unsupported-recipe check, and return the shared
  customModelApplyErrorMessage when the kind is unsupported. A non-claude
  CLI pointing at an Anthropic-type endpoint never reaches the
  GET /running probe or a requiresConfirmation answer, so the caller is no
  longer asked to approve evicting another session's model for an apply
  that cannot happen (API-only path; the Run menu already hides the rows)
- Route tests in both files pair the refusal with a /running mock that
  WOULD ask for confirmation (ready model + conflicting session) and pin
  that /running was never fetched
- test/custom-model-recovery-warning.test.ts stops the WebServer it
  constructs, in line with every other test that builds one
@opticon454

Copy link
Copy Markdown
Contributor Author

Rebased onto 1.48.0 (2aa6016d) and force-pushed. The conflicts were with #752, #755 and #757 in the endpoint-type and default-context-length code; I kept both sides in each, and boot recovery keeps the Anthropic-type warning on top of the new AppliedCustomModel return. Nothing else changed, and the two open design points are untouched.

Typecheck, lint and format are clean. Full npm test: 14,127 passed, 1 failed (frontend-public-tooling, which needs the gitignored spreadsheet asset bundle that my test copy does not have).

@opticon454
opticon454 force-pushed the feature/custom-endpoint-anthropic branch from e5fa234 to bc20c7d Compare November 5, 2026 00:15
@Ark0N

Ark0N commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thank you @opticon454 for the rebase onto 1.48.0. This PR adds an endpointType (openai or anthropic) to saved custom model endpoints; with an Anthropic-type endpoint, Claude Code's ANTHROPIC_BASE_URL gets the gateway's /anthropic root, and every other CLI refuses it with one shared message and drops out of the Run menu.

The conflict resolution is right in the code: boot recovery keeps its warning on top of the new AppliedCustomModel return, and both routes still refuse the mismatch before the llama-swap probe. CI is green, and typecheck, lint, format, the frontend syntax check, the browser-exclude check, the CLI catalogue check and the custom-model, render and route test files pass here.

The rebase left a few things behind:

  1. DeepSeek is a configDir recipe now. 1.48.0 changed it (src/config/cli-registry/stock.ts:1575), so the lists that name it as an env recipe are out of date, and the new paragraph at docs/architecture-invariants.md:641 contradicts line 639 above it. Please move it to the configDir list there and at src/config/cli-registry/types.ts:888, src/custom-model-injection.ts:156 and :188, and test/custom-model-injection.test.ts:322. The behaviour is correct; only the wording is stale.
  2. Two comments from master were dropped (src/web/routes/session-routes.ts:1582 and :4517). The early-refusal hunks replaced the "Discovery's per-model figure wins" and "Per-model discovery figure first" comments, while the resolveCustomModelContextLength calls they explain are still there. Please restore both above those calls.
  3. Small ones: docs/api-reference.md:1230 and :1238 lost the wrap from round two; test/custom-model-recovery-warning.test.ts:60 and :65 still type _rebuildCustomModelEnv as Record<string, string> | undefined, and it returns AppliedCustomModel | undefined now; the comments at src/custom-model-injection-apply.ts:325 and src/web/server.ts:1580 leave out the endpoint type mismatch.

Both design points are still open on my side, unchanged:

  1. Whether the injected base hardcodes Bifrost's /anthropic segment (withAnthropicRootSuffix, src/custom-model-injection.ts:130) or treats the saved base URL as the Anthropic base and only normalises /v1.
  2. Whether a Claude Code only endpoint type earns its place, since an OpenAI-type endpoint already points Claude Code's ANTHROPIC_BASE_URL at the raw saved base URL, and the type is set per endpoint while what differs is per CLI.

Please keep holding off on the path handling, the help text at src/web/public/index.html:2837, the discovery sentence and the wiki pages. Your read on whether this PR should stay, be relabelled for a gateway route, or be dropped is welcome here.

The two open points come first. Once they are answered, what remains is aligning the code, help text, docs and wiki pages with that answer, plus the items above.

…ents and rewrap the endpoint docs after the rebase
@opticon454

Copy link
Copy Markdown
Contributor Author

Pushed 5da33ef, thanks. All the leftovers from the rebase are done:

  1. DeepSeek is listed as a configDir recipe in docs/architecture-invariants.md (the new paragraph no longer contradicts the one above it), src/config/cli-registry/types.ts, both comments in src/custom-model-injection.ts, and the comment in test/custom-model-injection.test.ts.
  2. The two dropped comments are back above their resolveCustomModelContextLength calls in session-routes.ts (the swap route and quick-start).
  3. docs/api-reference.md: the POST and PUT endpoint entries are rewrapped. test/custom-model-recovery-warning.test.ts types _rebuildCustomModelEnv as AppliedCustomModel | undefined. The comments in custom-model-injection-apply.ts and server.ts now mention an endpoint type mismatch.

Typecheck, lint and format are clean, and the six custom-model test files pass (220 tests). The two design points are untouched, and I have not touched the path handling, help text, discovery sentence or wiki pages.

@Ark0N

Ark0N commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thank you @opticon454 for clearing up the rebase leftovers. This PR adds an endpointType (openai or anthropic) to saved custom model endpoints; with an Anthropic-type endpoint, Claude Code's ANTHROPIC_BASE_URL gets the gateway's /anthropic root, and every other CLI refuses it with one shared message and drops out of the Run menu.

All of the last round is in: DeepSeek sits in the configDir lists, both resolveCustomModelContextLength comments are back, the two docs/api-reference.md entries are rewrapped, the recovery test types AppliedCustomModel | undefined, and the comments in src/custom-model-injection-apply.ts and src/web/server.ts name the type mismatch. CI is green, and typecheck, lint, format, the frontend syntax check, the browser-exclude check, the CLI catalogue check and the full npm test pass here.

Both design points are still open on my side, unchanged:

  1. Whether the injected base hardcodes Bifrost's /anthropic segment (withAnthropicRootSuffix, src/custom-model-injection.ts:130) or treats the saved base URL as the Anthropic base and only normalises /v1.
  2. Whether a Claude Code only endpoint type earns its place, since an OpenAI-type endpoint already points Claude Code's ANTHROPIC_BASE_URL at the raw saved base URL, and the type is set per endpoint while what differs is per CLI.

Please keep holding off on the path handling, the help text at src/web/public/index.html:2837, the discovery sentence and the wiki pages.

One fact bears on both points, and you have the gateway to check it: does GET <gateway>/anthropic/v1/models answer on Bifrost, and in what shape? Bifrost's Anthropic SDK page gives /anthropic as the base URL and documents no model listing under it. If that route answers like /v1/models, an OpenAI-type endpoint saved at <gateway>/anthropic already gives Claude Code the same ANTHROPIC_BASE_URL with working discovery. If it does not, saving the bare root and appending the suffix is the only form that can do both. Your read on whether this PR should stay, be relabelled for a gateway route, or be dropped is welcome here too.

If the PR stays, these small items apply whichever way the points are answered:

  1. docs/api-reference.md:1249: a PUT without endpointType resets an Anthropic endpoint to openai, the same whole-record replace authStyle gets, and "follows the POST semantics" does not say so. Please state it outright, and add one app.inject case next to updates endpointType on PUT (test/routes/custom-model-routes.test.ts:122) that pins the reset.
  2. src/web/routes/session-routes.ts:1697 and :4618: since the early refusal, these if (!applied) branches exist only for type narrowing, so the comments describe a path that no longer runs. Please keep the guards and say that instead.
  3. src/custom-model-injection-apply.ts:314, test/custom-model-run-menu-ui.test.ts:236 and src/web/public/session-ui.js:930: these comments still describe this PR's history (a rejected wording, the narrowing, what the menu used to offer). Please state the code facts only.
  4. src/web/server.ts:2074: entry.id as string can go back to entry.id.

The two open points come first. Once they are answered, what remains is aligning the code, help text, docs and wiki pages with that answer, plus the items above.

@Ark0N

Ark0N commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thank you @opticon454 for the latest revision. This PR adds an endpointType (openai or anthropic) to saved custom model endpoints; with an Anthropic-type endpoint, Claude Code's ANTHROPIC_BASE_URL gets the gateway's /anthropic root, and every other CLI refuses it with one shared message and drops out of the Run menu.

All of the last round is in: the PUT reset is stated in docs/api-reference.md and pinned by a test, the if (!applied) comments describe what those guards do now, the history comments are gone from the three files I listed, and entry.id is back. CI is green, and typecheck, lint, format, the frontend syntax check, the browser-exclude check, the CLI catalogue check and the custom-model, render and route test files pass here.

Both design points are still open on my side, unchanged:

  1. Whether the injected base hardcodes Bifrost's /anthropic segment (withAnthropicRootSuffix, src/custom-model-injection.ts:130) or treats the saved base URL as the Anthropic base and only normalises /v1.
  2. Whether a Claude Code only endpoint type earns its place, since an OpenAI-type endpoint already points Claude Code's ANTHROPIC_BASE_URL at the raw saved base URL, and the type is set per endpoint while what differs is per CLI.

Please keep holding off on the path handling, the help text at src/web/public/index.html:2837, the discovery sentence and the wiki pages.

On the question from my last comment, Bifrost's source at main (b84656d) gives part of the answer. This comes from reading the code, not from a running gateway:

  • GET /anthropic/v1/models is registered (transports/bifrost-http/integrations/anthropic.go:1691).
  • It answers in the Anthropic list shape: data[].id, display_name, max_input_tokens, max_tokens. Codeman's discovery reads data[].id, so it gets ids, but there is no context_length or context_window, the two fields src/web/routes/custom-model-routes.ts:356 reads.
  • ToAnthropicListModelsResponse strips a known provider prefix from each id (core/providers/anthropic/models.go:90), so openai/qwen3 is listed as qwen3.
  • With a direct key and no x-bf-model-provider header, the listing asks only the Anthropic provider, and answers 400 if that provider is not configured (transports/bifrost-http/integrations/router.go:1046).

You have the gateway, so please run GET <gateway>/anthropic/v1/models and GET <gateway>/v1/models on your build and post what differs: the ids, any context figures, and whether Claude Code reaches the right model with an id from the first list. Your read on whether this PR should stay, be relabelled for a gateway route, or be dropped is welcome here too.

If the PR stays, these small items apply whichever way the points are answered:

  1. src/web/public/settings-ui.js:3995 and :4033: the new dropdown has no test. Please add one JSDOM case beside test/custom-model-run-menu-ui.test.ts that opens the editor on an Anthropic host and on a legacy host, saves, and asserts the endpointType in the request body.
  2. test/custom-model-recovery-warning.test.ts:75: "the case the old hardcoded wording lied about" still describes the PR's history. Please state the code fact only.
  3. docs/api-reference.md:1251: this line runs to 96 characters in a block wrapped at about 78. Please move `defaultModelId`, when set, to the next line.

If withAnthropicRootSuffix survives the first point, its regexes need to test the URL's path and not the whole string: http://anthropic reads as already suffixed today and gets no /anthropic.

The two open points come first. Once they are answered, what remains is aligning the code, help text, docs and wiki pages with that answer, plus the items above.

@opticon454

Copy link
Copy Markdown
Contributor Author

Pushed 0bf45b7, thanks. The three small items are in:

  1. test/custom-model-endpoint-editor-ui.test.ts (new, JSDOM, same approach as the run-menu test): it opens the editor on an Anthropic host and on a legacy host, saves each, and asserts the endpointType in the PUT body (anthropic and openai).
  2. The comment in test/custom-model-recovery-warning.test.ts states the code fact only.
  3. The defaultModelId sentence in docs/api-reference.md is on its own line.

On the gateway question: I have not been able to get a successful GET /anthropic/v1/models yet, because one of the providers behind my Bifrost is down and the listing returns 502 on both /v1/models and /anthropic/v1/models. I will post the ids and any context figures once it answers. The two design points are untouched, and I have not touched the path handling, help text, discovery sentence or wiki pages.

@Ark0N

Ark0N commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thank you @opticon454 for this revision. This PR adds an endpointType (openai or anthropic) to saved custom model endpoints; with an Anthropic-type endpoint, Claude Code's ANTHROPIC_BASE_URL gets the gateway's /anthropic root, and every other CLI refuses it with one shared message and drops out of the Run menu.

All of the last round is in: test/custom-model-endpoint-editor-ui.test.ts covers the dropdown on an Anthropic host and a legacy host, the recovery test comment states the code fact, and the docs/api-reference.md line is rewrapped. Typecheck, lint, format, the frontend syntax check, the browser-exclude check, the CLI catalogue check and the custom-model and CLI registry test files pass here.

Both design points are still open on my side, unchanged:

  1. Whether the injected base hardcodes Bifrost's /anthropic segment (withAnthropicRootSuffix, src/custom-model-injection.ts:130) or treats the saved base URL as the Anthropic base and only normalises /v1.
  2. Whether a Claude Code only endpoint type earns its place, since an OpenAI-type endpoint already points Claude Code's ANTHROPIC_BASE_URL at the raw saved base URL, and the type is set per endpoint while what differs is per CLI.

One measurement for both: Claude Code 2.1.318 against a local logging server sent POST /anthropic/v1/messages?beta=true with the Anthropic-type base and POST /v1/messages?beta=true with an OpenAI-type base saved at the root, so for a root base the two options differ by that path segment.

Please keep holding off on the path handling, the help text at src/web/public/index.html:2837, the discovery sentence and the wiki pages. Thanks for trying the listing. Once your gateway answers, the GET <gateway>/anthropic/v1/models and GET <gateway>/v1/models comparison from my last comment is the missing fact, and your read on whether this PR should stay, be relabelled for a gateway route, or be dropped is welcome here too.

If the PR stays, these small items apply whichever way the points are answered:

  1. src/web/routes/session-routes.ts:1588 and :4522: the early refusal builds the injection a second time. The existing builds at :1611 and :4537 already run before the llama-swap probe, with only the context-length and slot-count lookups in between, so please move the unsupported refusal onto that result and drop the extra build. The unsupported arms of the ternaries at :1612 and :4538 go with it.
  2. src/web/public/settings-ui.js:3968: the endpoint list shows the base URL and model summary but not the type, so an Anthropic-type endpoint looks like any other while its other CLIs are missing from the Run menu. Please show the type on that row for an Anthropic-type endpoint.
  3. test/custom-model-endpoint-editor-ui.test.ts:27: the fixture declares its own <select>, and no test reads the real one at src/web/public/index.html:2838. If that id changes, openCustomModelHostEditor throws on a null element and the editor stops opening for every endpoint. Please add one assertion against the real index.html for the id and both option values.

If withAnthropicRootSuffix survives the first point, the path check from my last comment also has to cover a query string: http://gw:8080/x?token=1 becomes http://gw:8080/x?token=1/anthropic today. Parsing with new URL() and working on pathname covers that and the http://anthropic case.

The two open points come first. Once they are answered, what remains is aligning the code, help text, docs and wiki pages with that answer, plus the items above.

@opticon454

Copy link
Copy Markdown
Contributor Author

Applied the three small items in 54cde4c3:

  1. session-routes.ts: the unsupported refusal now sits on the existing build result (before the llama-swap probe) in both the swap route and quick-start. The extra build and the unsupported arms of the two ternaries are gone.
  2. settings-ui.js: an Anthropic-type endpoint row now ends with "Anthropic (Claude Code only)".
  3. test/custom-model-endpoint-editor-ui.test.ts: one new assertion loads the real index.html and checks the customModelHostEndpointType id and both option values.

Typecheck, format and the custom-model route, editor, run-menu and recovery test files pass. I have not run the full suite.

The two design points and the withAnthropicRootSuffix URL parsing are still open and waiting on my decision. The gateway listing still cannot be run, because Bifrost returns 502 on both routes until its failing provider is fixed.

@Ark0N

Ark0N commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thank you @opticon454 for this revision. This PR adds an endpointType (openai or anthropic) to saved custom model endpoints; with an Anthropic-type endpoint, Claude Code's ANTHROPIC_BASE_URL gets the gateway's /anthropic root, and every other CLI refuses it with one shared message and drops out of the Run menu.

All of the last round is in: both routes refuse the mismatch on the existing build result with the extra build gone, an Anthropic-type endpoint is marked on its list row, and test/custom-model-endpoint-editor-ui.test.ts reads the real index.html. Typecheck, lint, format, the frontend syntax check, the browser-exclude check and the CLI catalogue check pass here.

Both design points are still open on my side, unchanged:

  1. Whether the injected base hardcodes Bifrost's /anthropic segment (withAnthropicRootSuffix, src/custom-model-injection.ts:130) or treats the saved base URL as the Anthropic base and only normalises /v1.
  2. Whether a Claude Code only endpoint type earns its place, since an OpenAI-type endpoint already points Claude Code's ANTHROPIC_BASE_URL at the raw saved base URL, and the type is set per endpoint while what differs is per CLI.

Please keep holding off on the path handling, the help text at src/web/public/index.html:2837, the discovery sentence and the wiki pages.

Two things are still outstanding from your side: the GET <gateway>/anthropic/v1/models and GET <gateway>/v1/models comparison once your gateway answers (the ids, any context figures, and whether Claude Code reaches the right model with an id from the first list), and your read on whether this PR should stay, be relabelled for a gateway route, or be dropped.

If the PR stays, these small items apply, and none of them needs doing before the two points are answered:

  1. src/custom-model-injection.ts:130: if withAnthropicRootSuffix survives the first point, parse with new URL() and work on pathname. Today http://anthropic comes back with no suffix and http://gw:8080/x?token=1 becomes http://gw:8080/x?token=1/anthropic. Please add both inputs as tests.
  2. src/web/public/settings-ui.js:3968: the Anthropic marker on the list row has no test. One JSDOM case in test/custom-model-endpoint-editor-ui.test.ts for an Anthropic host and a legacy host covers it.
  3. src/web/server.ts:2063 and src/web/routes/session-routes.ts:1603: these comments say a refused click cannot happen and call the refusal API-only, but a menu opened before an endpoint was switched to Anthropic can still send the request. Please state that the menu hides these rows and the server refuses whatever still arrives.
  4. docs/custom-model-endpoints.md:63: this line stops at 38 characters; please rewrap the paragraph.

Nothing else is needed on the branch until the two points are answered.

@opticon454

Copy link
Copy Markdown
Contributor Author

Applied items 2 to 4 in fb6f1fc9:

  1. test/custom-model-endpoint-editor-ui.test.ts: a JSDOM case renders the endpoint list for an Anthropic host and a legacy host and checks that only the Anthropic row carries "Anthropic (Claude Code only)".
  2. server.ts and session-routes.ts: both comments now say the menu hides these rows and the server refuses whatever still arrives, including a menu opened before the endpoint was switched to Anthropic.
  3. docs/custom-model-endpoints.md: the paragraph is rewrapped.

Item 1 (new URL() in withAnthropicRootSuffix, with the http://anthropic and query-string tests) waits on the first design point, as you said. Typecheck, format and the editor test file pass; I have not run the full suite.

Both design points and the gateway comparison are still open on my side. Bifrost still returns 502 on the listing routes while one of its providers is down.

@Ark0N

Ark0N commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thank you @opticon454 for this revision. This PR adds an endpointType (openai or anthropic) to saved custom model endpoints; with an Anthropic-type endpoint, Claude Code's ANTHROPIC_BASE_URL gets the gateway's /anthropic root, and every other CLI refuses it with one shared message and drops out of the Run menu.

Items 2 and 3 from my last comment are in: test/custom-model-endpoint-editor-ui.test.ts covers the list-row marker on an Anthropic host and a legacy host, and the comments in src/web/server.ts and src/web/routes/session-routes.ts now say the menu hides these rows and the server refuses whatever still arrives. CI is green, and typecheck, lint, format, the frontend syntax check, the browser-exclude check and the CLI catalogue check pass here.

One leftover from item 4: docs/custom-model-endpoints.md:65 still stops at 39 characters ("parse. Model discovery ignores the type"). Please join it with the line after it. This is wrapping only, and the wording stays on hold with the discovery sentence.

Both design points are still open on my side, unchanged:

  1. Whether the injected base hardcodes Bifrost's /anthropic segment (withAnthropicRootSuffix, src/custom-model-injection.ts:130) or treats the saved base URL as the Anthropic base and only normalises /v1.
  2. Whether a Claude Code only endpoint type earns its place, since an OpenAI-type endpoint already points Claude Code's ANTHROPIC_BASE_URL at the raw saved base URL, and the type is set per endpoint while what differs is per CLI.

Please keep holding off on the path handling, the help text at src/web/public/index.html:2837, the discovery sentence and the wiki pages.

Two things are still outstanding from your side: the GET <gateway>/anthropic/v1/models and GET <gateway>/v1/models comparison once your gateway answers (the ids, any context figures, and whether Claude Code reaches the right model with an id from the first list), and your read on whether this PR should stay, be relabelled for a gateway route, or be dropped.

If the PR stays, these small items apply, and none of them needs doing before the two points are answered:

  1. src/custom-model-injection.ts:130: the new URL() and pathname change with its two test inputs, carried over from my last comment.
  2. test/routes/session-custom-model.test.ts:145: all four new route cases, these two and the two at test/routes/quick-start-custom-model.test.ts:198 and :216, are codex refusals. Please add one app.inject case that applies an Anthropic-type endpoint to a claude session and asserts the ANTHROPIC_BASE_URL passed to setCustomModel.
  3. src/web/public/index.html:2837: when the help text is written, "OpenAI works with every CLI" needs to change, because antigravity has no custom-model recipe (src/config/cli-registry/stock.ts:1097).

Nothing else is needed on the branch until the two points are answered.

@opticon454

Copy link
Copy Markdown
Contributor Author

Done in ff72f5ea:

  • docs/custom-model-endpoints.md: the short line is joined with the one after it (wrapping only, wording unchanged).
  • Item 2: test/routes/session-custom-model.test.ts has an app.inject case that applies an Anthropic-type endpoint to a claude session and asserts ANTHROPIC_BASE_URL is http://192.168.1.50:8080/anthropic in the setCustomModel call. That test file passes (45 tests) and prettier is clean; I have not run the full suite.

Items 1 and 3 stay on hold with the two design points. The Bifrost listing comparison is still blocked by the 502 from a provider behind it.

@Ark0N

Ark0N commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thank you @opticon454 for this revision. This PR adds an endpointType (openai or anthropic) to saved custom model endpoints; with an Anthropic-type endpoint, Claude Code's ANTHROPIC_BASE_URL gets the gateway's /anthropic root, and every other CLI refuses it with one shared message and drops out of the Run menu.

All of the last round is in: docs/custom-model-endpoints.md is rewrapped, and test/routes/session-custom-model.test.ts applies an Anthropic-type endpoint to a claude session and asserts the ANTHROPIC_BASE_URL passed to setCustomModel. Typecheck, lint, format, the frontend syntax check, the browser-exclude check and the CLI catalogue check pass here.

Both design points are still open on my side, unchanged:

  1. Whether the injected base hardcodes Bifrost's /anthropic segment (withAnthropicRootSuffix, src/custom-model-injection.ts:130) or treats the saved base URL as the Anthropic base and only normalises /v1.
  2. Whether a Claude Code only endpoint type earns its place, since an OpenAI-type endpoint already points Claude Code's ANTHROPIC_BASE_URL at the raw saved base URL, and the type is set per endpoint while what differs is per CLI.

One fact bears on both. The Claude Code setup docs of three other Anthropic-protocol servers use a base with no /anthropic segment: OpenRouter (https://openrouter.ai/api), LiteLLM (the proxy root) and Ollama (http://localhost:11434). Running the builder on OpenRouter's base gives https://openrouter.ai/api for the OpenAI type and https://openrouter.ai/api/anthropic for the Anthropic type. This comes from those docs and the builder, not from a run against those services.

Please keep holding off on the path handling, the help text at src/web/public/index.html:2837, the discovery sentence and the wiki pages.

Two things are still outstanding from your side: the GET <gateway>/anthropic/v1/models and GET <gateway>/v1/models comparison once your gateway answers (the ids, any context figures, and whether Claude Code reaches the right model with an id from the first list), and your read on whether this PR should stay, be relabelled for a gateway route, or be dropped.

If the PR stays, these small items apply, and none of them needs doing before the two points are answered:

  1. src/custom-model-injection.ts:130: the new URL() and pathname change with its two test inputs, carried over from my last comment.
  2. src/web/public/index.html:2837: when the help text is written, "OpenAI works with every CLI" needs to change, because antigravity has no custom-model recipe (src/config/cli-registry/stock.ts:1097).
  3. docs/api-reference.md:1297: please add one clause saying an Anthropic-type endpoint on a CLI that cannot use it answers OPERATION_FAILED with the shared message. The quick-start entry at :1338 already says it runs the identical checks.
  4. The Verification section of the PR description gives test totals from before the rebase. Please refresh it after your next full npm test.

Nothing else is needed on the branch until the two points are answered. Once they are answered, what remains is aligning the code, help text, docs and wiki pages with that answer, plus the items above.

@opticon454

Copy link
Copy Markdown
Contributor Author

Items 3 and 4 are done:

  • docs/api-reference.md: the custom-model route entry now says an Anthropic-type endpoint on a CLI that cannot use it answers OPERATION_FAILED with the shared refusal message, before any llama-swap check (8366e928).
  • The Verification section of the description is refreshed from a full npm test at 8366e928: 689 files and 14,133 tests passed, 12 skipped, 1 failed. The one failure is test/frontend-public-tooling.test.ts, which needs gitignored spreadsheet assets missing from my test checkout.

Items 1 and 2 stay on hold with the two design points. The Bifrost listing comparison is still blocked by a 502 from a provider behind it.

@opticon454

Copy link
Copy Markdown
Contributor Author

The gateway answers now. Both listings were read with GET and the same bearer key (Bifrost, one provider behind it: llama-swap, 50 models).

  • GET <gateway>/v1/models returns 200 with data[] items carrying id, created, owned_by and context_length (131072 on the first four). Ids are provider-prefixed, e.g. llama-swap/gemma-4-26b-a4b-ud-q4_k_xl.
  • GET <gateway>/anthropic/v1/models returns 200 in the Anthropic list shape: data[] items carrying id, type: "model", display_name (empty) and created_at, plus has_more: false. There is no context field of any kind.
  • The two lists hold the same 50 models. Every Anthropic id is the OpenAI id with the llama-swap/ prefix removed (checked as a set comparison), e.g. gemma-4-26b-a4b-ud-q4_k_xl.

So on this gateway the Anthropic route lists no context figure, and discovery has to stay on the OpenAI listing for it, which matches the current behaviour. The id on the Anthropic route is the bare model name, while the OpenAI route needs the prefix.

I did not send a POST /anthropic/v1/messages to check that Claude Code reaches the right model with a bare id, because that would make llama-swap load a model and could evict someone's. I can run that with a small model if you want it.

My read on the design points is still to come; I will post it once I have decided.

@opticon454

Copy link
Copy Markdown
Contributor Author

Follow-up to the listing comparison: I sent POST <gateway>/anthropic/v1/messages (max_tokens 16 to 32, "Say hi.") with the smallest Gemma model, using both id forms.

  • The bare id from the Anthropic list (gemma-4-e2b-it-ud-q8_k_xl) answered 200 with a normal Anthropic message, stop_reason: end_turn, and the response model field was the same GGUF file as for the prefixed id.
  • The prefixed id from the OpenAI list (llama-swap/gemma-4-e2b-it-ud-q8_k_xl) also answered 200 and reached that same model.
  • Some replies came back with an empty content array and some with text, whichever id was used. That is the small model, not the route.

So on this gateway the Anthropic route accepts either id form, and an id taken from the OpenAI list works there as is. I ran this with plain HTTP requests, not through Claude Code, so I have not shown Claude Code reaching it end to end.

@Ark0N

Ark0N commented Nov 5, 2026

Copy link
Copy Markdown
Owner

Thank you @opticon454 for this revision. This PR adds an endpointType (openai or anthropic) to saved custom model endpoints; with an Anthropic-type endpoint, Claude Code's ANTHROPIC_BASE_URL gets the gateway's /anthropic root, and every other CLI refuses it with one shared message and drops out of the Run menu.

Items 3 and 4 from my last comment are in: docs/api-reference.md:1309 states the OPERATION_FAILED refusal, and the Verification section is refreshed. CI is green, and typecheck, lint, format, the frontend syntax check, the browser-exclude check, the CLI catalogue check and the full npm test (14,134 passed, 12 skipped, 0 failed) pass here. test/frontend-public-tooling.test.ts passes here too, so the one failure in your run came from your test checkout, as you said.

Both design points are still open on my side, unchanged:

  1. Whether the injected base hardcodes Bifrost's /anthropic segment (withAnthropicRootSuffix, src/custom-model-injection.ts:130) or treats the saved base URL as the Anthropic base and only normalises /v1.
  2. Whether a Claude Code only endpoint type earns its place, since an OpenAI-type endpoint already points Claude Code's ANTHROPIC_BASE_URL at the raw saved base URL, and the type is set per endpoint while what differs is per CLI.

Please keep holding off on the path handling, the help text at src/web/public/index.html:2837, the discovery sentence and the wiki pages.

Two things are still outstanding from your side: the GET <gateway>/anthropic/v1/models and GET <gateway>/v1/models comparison once your gateway answers (the ids, any context figures, and whether Claude Code reaches the right model with an id from the first list), and your read on whether this PR should stay, be relabelled for a gateway route, or be dropped.

If the PR stays, these small items apply, and none of them needs doing before the two points are answered:

  1. src/custom-model-injection.ts:130: the new URL() and pathname change with its two test inputs, carried over from my last comment. Both inputs (http://anthropic and http://gw:8080/x?token=1) pass the save schema, so a user can reach them from the editor.
  2. src/web/public/index.html:2837: when the help text is written, "OpenAI works with every CLI" needs to change, because antigravity has no custom-model recipe (src/config/cli-registry/stock.ts:1097).
  3. docs/custom-model-endpoints.md:59, docs/api-reference.md:1234, docs/architecture-invariants.md:641, src/custom-model-hosts.ts:20, src/custom-model-injection.ts:48 and src/web/schemas.ts:2443: these say endpointType picks the wire protocol. Claude Code speaks the Anthropic Messages protocol under both options, so when this wording is written, please state what the field does. The same goes for "never the OpenAI /v1 route" at src/config/cli-registry/stock.ts:405: an OpenAI-type endpoint gives Claude Code the raw saved base, with no /v1 added.

Nothing else is needed on the branch until the two points are answered.

@opticon454

Copy link
Copy Markdown
Contributor Author

End-to-end run with Claude Code 2.1.318 against the Bifrost gateway (claude -p, isolated CLAUDE_CONFIG_DIR, --dangerously-skip-permissions). The environment came from this branch's own buildCustomModelInjection for the real claude registry entry, so the base URL, key and model variables are what a Codeman session would get. Results:

Case Endpoint type, saved base Model id Result
Plain prompt anthropic, gateway root bare id (small model) ok, pong, base became <gw>/anthropic
Read tool anthropic, gateway root bare id ok, 2 turns, read the file and returned its secret word
Bash tool anthropic, gateway root bare id ok, 2 turns, command ran and out.txt exists on disk
Prefixed id anthropic, gateway root llama-swap/... id from the OpenAI list ok, pong
Resume anthropic, gateway root bare id ok, --resume recalled the number from turn 1
OpenAI type openai, gateway root bare id fails: API Error: 405 Method Not Allowed (base stays <gw>, so Claude Code posts to /v1/messages on the root)
OpenAI type, base saved with /anthropic openai, <gw>/anthropic bare id ok, pong
Anthropic type, base saved with /anthropic anthropic, <gw>/anthropic bare id ok, pong (suffix is idempotent)

What this says for the two design points:

  • On this gateway an OpenAI-type endpoint saved at the root does not work with Claude Code (405), and one saved with /anthropic in the base does. The Anthropic type is what makes the root form work; without it the user has to know to type /anthropic into the base, and that same base would then break the OpenAI listing used for discovery.
  • Both id forms reach the right model through Claude Code, so an id from the OpenAI listing can be used as is.

Not covered: these runs used claude -p with a scratch config dir, so they did not go through a Codeman session's apply, restart, trust-file seeding or the interactive UI, and not other gateways. Claude Code printed [claude-code:unrecognized_model] for the gateway model id in every run (it appends [1m]); requests still succeeded.

I have no view posted yet on the two design points; this is the evidence for them.

@opticon454

Copy link
Copy Markdown
Contributor Author

One correction to my end-to-end comment: I wrote that saving the base with /anthropic "would then break the OpenAI listing used for discovery". That is too strong. <gw>/anthropic/v1/models answers 200 (earlier comment), so discovery would not fail; it would read the Anthropic-shaped list instead, which has bare ids and no context field. The cost is losing the per-model context figure, not a broken listing. I have not run discovery through Codeman against that base, so that part is inferred from the two listings.

@opticon454

Copy link
Copy Markdown
Contributor Author

Measured evidence for the first design point, using Codeman's own discovery route and Claude Code 2.1.318 against the Bifrost gateway (no guessing from the code).

1. Claude Code needs CLAUDE_CODE_MAX_CONTEXT_TOKENS, and honours it. /context for the same model id (llama-swap/qwen2.5-coder-7b-instruct-q4_k_m, gateway figure 32768):

Launch Window Claude Code reports
--model <id>, variable unset 17.2k / 200k
--model <id>, variable 32768 17.2k / 32.8k
--model sonnet / opus / haiku (the alias path), variable 32768 17.2k / 32.8k
no --model at all, variable 32768 17.2k / 1m (variable ignored; Claude Code appends [1m] to the alias itself)

So the figure is honoured whenever a --model is passed, which Codeman does from claudeModel. Without it Claude Code assumes 200k, and the gateway really does have models at 16384 and 32768 (1 and 5 models; 43 are 131072).

2. Where Codeman gets that figure. I ran Codeman's real POST /api/model-endpoints/:id/discover-models (isolated data dir, spare port) against three saved endpoints for the same gateway:

Saved base Type Models Models with a context figure
gateway root anthropic 50 49
gateway root openai 50 49
<gateway>/anthropic openai 50 0

Discovery reads <base>/v1/models, and only the OpenAI listing at the root carries context_length; /anthropic/v1/models has none (0 of 50). So an OpenAI-type endpoint saved with /anthropic in its base works for requests (earlier table) but gets no context figure for any model, so Claude Code assumes 200k and never compacts for the 16k and 32k models.

3. Conclusion on this gateway. With the base at the root, Claude Code needs /anthropic on ANTHROPIC_BASE_URL (405 otherwise), discovery needs the root. One saved base cannot serve both unless Codeman derives the Anthropic path from it, which is what the Anthropic type does. This is for Bifrost only; I have not tested OpenRouter, LiteLLM or Ollama.

@opticon454

opticon454 commented Nov 6, 2026 •

Copy link
Copy Markdown
Contributor Author

Closing this one. I tested Claude Code (2.1.318) against eight servers and only Bifrost needs /anthropic. On every other server the Anthropic type would break Claude Code, so the type is not worth carrying. The Bifrost route is being taken up with Bifrost separately.

Claude Code against each server. "Root" means ANTHROPIC_BASE_URL is the server's own root with nothing added, i.e. what an OpenAI-type endpoint injects. "Suffix" is what the Anthropic type injects (root + /anthropic). The environment came from this branch's own builder.

Server POST <root>/v1/messages POST <root>/anthropic/v1/messages Claude Code, root Claude Code, suffix
llama-swap 200 404 works 404, "issue with the selected model"
llama.cpp llama-server 200 404 works 404, same
vLLM (CPU build) 200 404 works (needs --enable-auto-tool-choice --tool-call-parser) 404, same
Ollama 200 404 works 404, same
LiteLLM proxy 1.108.1 200 401 works, including the Read tool "Invalid API key"
OpenRouter route present (401 without a key) 404 not run, no key would 404
Bifrost 405 200 405 works

So llama-swap, llama.cpp, vLLM, Ollama and LiteLLM all answer the Anthropic protocol natively at the root, which is where Claude Code already posts. Bifrost serves it only under its own /anthropic prefix. Notes on the table: LiteLLM was backed by llama-swap; llama.cpp, vLLM and Ollama ran a 0.5B model, which proves the route, not quality; OpenRouter was checked without a key, so only its two routes were probed.

Context size. Claude Code only honours CLAUDE_CODE_MAX_CONTEXT_TOKENS when a --model is passed (32.8k with the variable, 200k without, for a 32768 model). Codeman's real discover-models on each server found:

Endpoint Models With a context figure
Bifrost, root 50 49
Bifrost, base saved with /anthropic 50 0
llama-swap 7 3
OpenRouter 454 454
LiteLLM, Ollama, llama.cpp, vLLM 2, 1, 1, 1 0 each

The last row is a separate gap, not this PR: those four list a figure under other names (max_input_tokens, meta.n_ctx, max_model_len) which discovery does not read. I will open that separately if it is wanted.

For Bifrost in the meantime: a second endpoint saved as <gateway>/anthropic (OpenAI type) works for Claude Code, with the default context length set by hand.

@opticon454 opticon454 closed this Nov 6, 2026
@Ark0N

Ark0N commented Nov 6, 2026

Copy link
Copy Markdown
Owner

Thanks for measuring this before it shipped. The eight-server table settled it better than any review could have: only Bifrost needs /anthropic, and the type would have broken Claude Code on the rest. The workaround is in your comment for anyone who finds this later. The context figure gap (max_input_tokens, meta.n_ctx, max_model_len) is worth its own PR if you want to send it.

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