Skip to content

feat(claude): support custom model registry#1587

Open
guanghuang wants to merge 1 commit intocoleam00:devfrom
guanghuang:feat/claude-custom-model-registry
Open

feat(claude): support custom model registry#1587
guanghuang wants to merge 1 commit intocoleam00:devfrom
guanghuang:feat/claude-custom-model-registry

Conversation

@guanghuang
Copy link
Copy Markdown

@guanghuang guanghuang commented May 5, 2026

Summary

  • Problem: Claude provider workflows cannot define stable aliases for Claude-compatible gateways or local providers.
  • Why it matters: teams using provider gateways need per-model base URLs, credentials, and optional headers without repeating long model IDs in workflow YAML.
  • What changed: added a Claude model registry loaded from ~/.archon/claude-models.json, provider env injection, tests, docs, and a sanitized example file.
  • What did not change (scope boundary): built-in Claude models still pass through unchanged; no database, server, web UI, or workflow schema changes.

UX Journey

Before

User                   Archon Claude provider              Claude Code
────                   ──────────────────────              ───────────
sets model ─────────▶  passes model string directly ─────▶ uses default Claude endpoint
[!] gateway aliases, base URL, credentials, and headers must be managed outside Archon

After

User                         Archon Claude provider                         Claude Code
────                         ──────────────────────                         ───────────
defines ~/.archon/claude-models.json
uses gateway/gpt ─────────▶  [resolves alias to real model ID]
                             [injects provider env + headers] ───────────▶ calls selected gateway
receives normal stream ◀───  streams normalized Claude chunks ◀─────────── streams response

Architecture Diagram

Before

workflow/chat model
  │
  ▼
packages/providers/src/claude/provider.ts
  │ passes model/env directly
  ▼
@anthropic-ai/claude-agent-sdk

After

workflow/chat model
  │
  ▼
packages/providers/src/claude/provider.ts [~]
  │ reads alias/env from
  ▼
packages/providers/src/claude/model-registry.ts [+]
  │ loads
  ▼
~/.archon/claude-models.json [+ user file]
  │ returns resolved model + ANTHROPIC_* env
  ▼
@anthropic-ai/claude-agent-sdk

Connection inventory (list every module-to-module edge, mark changes):

From To Status Notes
provider.ts @anthropic-ai/claude-agent-sdk unchanged Still invokes Claude Code SDK with normalized options.
provider.ts model-registry.ts new Resolves custom aliases before SDK options are built.
model-registry.ts @archon/paths new Uses getArchonHome() to locate ~/.archon/claude-models.json.
model-registry.ts ~/.archon/claude-models.json new Optional user-owned provider/model catalog.
docs example file new Documents non-sensitive placeholder config.

Label Snapshot

  • Risk: risk: medium
  • Size: size: M
  • Scope: docs|tests|config
  • Module: providers:claude

Change Metadata

  • Change type: feature
  • Primary scope: multi

Linked Issue

Validation Evidence (required)

Commands and result summary:

bun run --cwd packages/providers test
bun run --cwd packages/providers type-check
git diff --check
  • Evidence provided (test/log/trace/screenshot): providers package test suite passed; providers TypeScript check passed; diff whitespace check passed.
  • If any command is intentionally skipped, explain why: full repository bun run validate was not run because this change is isolated to provider/docs files and the package-level provider suite covers the touched runtime code.

Security Impact (required)

  • New permissions/capabilities? No
  • New external network calls? No
  • Secrets/tokens handling changed? Yes
  • File system access scope changed? Yes

If any Yes, describe risk and mitigation:

The Claude provider can now read optional ~/.archon/claude-models.json and pass matched provider credentials to the Claude Code subprocess. The file is user-owned global config, built-in Claude models remain pass-through, example/docs use placeholders only, malformed configs degrade to pass-through with a warning, and request env remains the highest-precedence override.

Compatibility / Migration

  • Backward compatible? Yes
  • Config/env changes? Yes
  • Database migration needed? No

If yes, exact upgrade steps:

No migration is required. Users who want custom Claude-compatible providers can create ~/.archon/claude-models.json using the documented placeholder example and then set models such as gateway/gpt in config or workflow nodes.

Human Verification (required)

What was personally validated beyond CI:

  • Verified scenarios: alias resolution by provider/name, env injection for base URL/API key/custom headers, request env overriding registry env, pass-through for standard Claude models.
  • Edge cases checked: invalid JSON, missing providers object, malformed model entries, missing provider credentials, non-string header values, auth-token provider config.
  • What was not verified: live calls against a real third-party Claude-compatible gateway.

Side Effects / Blast Radius (required)

  • Affected subsystems/workflows: Claude provider runtime, provider tests, docs.
  • Potential unintended effects: a malformed custom registry could cause aliases not to resolve, but built-in model behavior remains pass-through.
  • Guardrails/monitoring for early detection: provider warning log claude.model_registry_load_error, unit tests for malformed config and pass-through behavior.

Rollback Plan (required)

  • Fast rollback command/path: revert this PR commit.
  • Feature flags or config toggles (if any): remove or rename ~/.archon/claude-models.json to disable custom alias resolution.
  • Observable failure symptoms: custom aliases pass through as raw model strings, gateway-specific env is absent, Claude Code returns model/auth errors.

Risks and Mitigations

  • Risk: custom provider credentials are read from a global user config file.
    • Mitigation: docs and example use placeholders only; no credentials are committed; env is injected only for matched custom models.
  • Risk: ambiguous model names across providers.
    • Mitigation: provider-scoped provider/name lookup is supported and recommended.
  • Risk: custom gateway header formatting may be wrong for some gateways.
    • Mitigation: registry encodes headers through Claude Code ANTHROPIC_CUSTOM_HEADERS, and tests cover header formatting.

Summary by CodeRabbit

  • New Features

    • Support for custom Claude-compatible providers and model aliases; resolved model IDs and provider-specific env/headers are injected into requests, with per-request env overrides taking precedence and passthrough when no alias matches.
  • Documentation

    • New guides and an example configuration for setting up Claude custom models, auth mappings, custom headers, and env-variable usage.
  • Tests

    • Expanded coverage for registry loading, validation, resolution, header handling, and env injection.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 5, 2026

📝 Walkthrough

Walkthrough

Adds a Claude custom model registry (reads ~/.archon/claude-models.json), resolves provider-scoped aliases (e.g., gateway/gpt) to concrete model IDs, injects provider-specific ANTHROPIC_* env vars and optional headers into Claude Code subprocesses, and adds tests, example config, and docs updates.

Changes

Custom Claude Model Registry & Provider Integration

Layer / File(s) Summary
Data Shape
packages/providers/src/claude/model-registry.ts
Adds ClaudeCustomModel, ClaudeCustomProvider, ClaudeModelsConfig, and ResolvedModel interfaces.
Validation Helpers
packages/providers/src/claude/model-registry.ts
Adds isNonEmptyString, header-name/value validators, and resolveConfigValue for env-var-name-or-literal resolution.
Core Implementation
packages/providers/src/claude/model-registry.ts
Implements ClaudeModelRegistry: loads/parses ~/.archon/claude-models.json, validates providers/models, records load/validation errors, normalizes credentials and headers.
Resolution Logic
packages/providers/src/claude/model-registry.ts
Adds resolve(modelRef) precedence rules (provider-scoped, exact id, case-insensitive name/id, passthrough) and buildResult producing ResolvedModel with env and optional headers.
Provider Wiring
packages/providers/src/claude/provider.ts
Imports and consults ClaudeModelRegistry in sendQuery before retries, may rewrite requestOptions.model, captures registryEnv for non-passthrough matches, refactors buildSubprocessEnv(envOverrides), and layers env as registryEnv then requestOptions.env.
Example Config
packages/providers/src/claude/claude-models.json.example
Adds example claude-models.json illustrating gateway and local providers, credentials, headers, and model entries.
Unit Tests — Registry
packages/providers/src/claude/model-registry.test.ts
Comprehensive tests for loading/validation errors, header filtering/unsafe rejection, matching precedence, env injection mapping to ANTHROPIC_*, credential isolation, getAll(), and getError().
Unit Tests — Provider
packages/providers/src/claude/provider.test.ts
Adds helper to write claude-models.json, refreshes mocked Archon home per test, and tests alias resolution, registry env/header injection, and request-level env overrides.
Test Manifest
packages/providers/package.json
Prepends bun test src/claude/model-registry.test.ts to package test script.
Documentation
packages/docs-web/src/content/docs/getting-started/ai-assistants.md, packages/docs-web/src/content/docs/reference/configuration.md
Adds docs describing ~/.archon/claude-models.json format, how to reference provider/name in YAML/workflows, mapping to ANTHROPIC_* env vars, and env-var-name vs literal resolution.

Sequence Diagram

sequenceDiagram
    participant User as Workflow/User
    participant Provider as Claude Provider
    participant Registry as ClaudeModelRegistry
    participant Subprocess as Claude Code Subprocess

    User->>Provider: sendQuery(model: "gateway/gpt", ...)
    Provider->>Registry: resolve("gateway/gpt")
    Registry->>Registry: read ~/.archon/claude-models.json and validate
    Registry-->>Provider: ResolvedModel { resolvedId, providerName, matchedBy, env }
    Provider->>Provider: merge registry env + request env
    Provider->>Subprocess: spawn Claude subprocess with model=resolvedId and env
    Subprocess-->>Provider: response
    Provider-->>User: result
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • coleam00/Archon#1518: Touches Claude provider internals; related to provider env and settingSources changes.
  • coleam00/Archon#1185: Refactors Claude provider implementation and capability handling related to sendQuery.
  • coleam00/Archon#1162: Prior decomposition of sendQuery and env/buildSubprocessEnv helpers that this PR extends.

Poem

🐰 A hop, a nudge, a model named,

gateway/gpt is now proclaimed.
Env stitched in like ribbon twine,
Headers kept and tokens fine.
Hooray — aliases find their home.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(claude): support custom model registry' clearly and concisely summarizes the main feature: adding a custom model registry for Claude providers.
Description check ✅ Passed The PR description comprehensively covers all required template sections: summary with problem/impact/changes/scope, detailed UX journey before/after, architecture diagram with connection inventory, appropriate labels, change metadata, linked issue reference, validation evidence, security impact analysis, compatibility/migration info, human verification details, side effects/blast radius, and rollback plan.
Linked Issues check ✅ Passed The PR fully addresses all coding requirements from issue #1586: alias resolution via provider/name format [#1586], env injection for ANTHROPIC_* variables [#1586], pass-through for built-in models [#1586], registry loading from ~/.archon/claude-models.json [#1586], and comprehensive tests and documentation [#1586].
Out of Scope Changes check ✅ Passed All changes are in-scope with the linked issue: model registry and provider integration (model-registry.ts, provider.ts updates), comprehensive test coverage (model-registry.test.ts, provider.test.ts updates), documentation (ai-assistants.md, configuration.md), and example config file. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (5)
packages/docs-web/src/content/docs/getting-started/ai-assistants.md (1)

132-170: ⚡ Quick win

Add a brief security note about secrets in claude-models.json.

The JSON example uses YOUR_GATEWAY_API_KEY as a placeholder, but new users may paste real keys/tokens directly without realizing the file lives in ~/.archon/ in plaintext and survives across sessions. A one-line callout — e.g. "Stored in plaintext in your home directory; ensure file permissions restrict access (chmod 600) and never commit this file." — matches the PR objective ("secrets are stored only in the user-owned config file") and parallels the placeholder warning already added in configuration.md Line 141.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/docs-web/src/content/docs/getting-started/ai-assistants.md` around
lines 132 - 170, Add a one-line security callout to the "Custom
Claude-Compatible Providers" section warning that the example secrets in
~/.archon/claude-models.json are stored in plaintext and persist across
sessions; instruct users to restrict file permissions (e.g., chmod 600) and
never commit the file or keys to version control, and place this sentence near
the JSON example or immediately before the paragraph describing
apiKey/authToken/baseUrl so it's clearly associated with claude-models.json and
the assistants.claude.model configuration.
packages/providers/src/claude/provider.ts (2)

941-971: 💤 Low value

Registry is re-read from disk on every sendQuery call.

new ClaudeModelRegistry() runs readFileSync against ~/.archon/claude-models.json synchronously on every invocation. In a workflow with many nodes (or under server load) this puts repeated synchronous disk I/O on the request path for a file that changes rarely. Consider lifting this to a process-level singleton (e.g. lazy-init module-scoped instance) or caching by file path with an fs.statSync mtime check for hot reload.

// model-registry.ts (sketch)
let cached: ClaudeModelRegistry | undefined;
export function getClaudeModelRegistry(): ClaudeModelRegistry {
  if (!cached) cached = new ClaudeModelRegistry();
  return cached;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/providers/src/claude/provider.ts` around lines 941 - 971, The code
instantiates a new ClaudeModelRegistry inside the request path (new
ClaudeModelRegistry()) causing synchronous disk reads on every sendQuery call;
fix it by introducing a module-scoped lazy singleton accessor (e.g.,
getClaudeModelRegistry()) that returns a cached ClaudeModelRegistry instance and
only re-creates it when the backing file changes (use fs.statSync mtime check
for hot-reload) and then replace occurrences of new ClaudeModelRegistry() in
provider.ts with calls to getClaudeModelRegistry(); keep using
registry.getError() and registry.resolve(rawModel) unchanged so logging and
registryEnv behavior remain the same.

949-952: ⚡ Quick win

Surface registry load errors to the caller, not just the log.

When claude-models.json is malformed and the user's model would have matched a registered alias, resolution silently falls through to passthrough and the Claude SDK reports a confusing "unknown model" error. The registry's parse error is only logged at warn level, so the actual root cause is buried. Consider yielding a system chunk with the registry error when one exists (mirroring the nodeConfigWarnings pattern at Lines 998–1000), so the user sees ⚠️ Claude model registry failed to load: … before the SDK error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/providers/src/claude/provider.ts` around lines 949 - 952, The
registry parse errors are only logged (registry.getError() -> getLog().warn) so
callers don't see them; update the code path that handles registryError to also
emit a system-facing chunk/message like the nodeConfigWarnings pattern (see
nodeConfigWarnings handling around nodeConfigWarnings usage) — e.g., when
registryError exists, in addition to getLog().warn({ error: registryError },
'claude.model_registry_load_error'), create/yield a system chunk or push a
system message containing "⚠️ Claude model registry failed to load:
{registryError.message}" so the caller sees the registry parse error before any
downstream "unknown model" SDK error.
packages/providers/src/claude/model-registry.ts (1)

184-206: 💤 Low value

Cross-provider id/name collisions resolve in Object.entries insertion order — document or scope explicitly.

Steps 2–4 iterate over all providers and return the first match. If two providers register the same model id (or name), the winner is determined by JSON insertion order, which is stable per ECMAScript but easy to silently flip when users edit the file. Either (a) document this precedence in the JSDoc above resolve(), or (b) prefer provider-scoped lookups (already covered by step 1) and warn / return a deterministic error when an unscoped reference is ambiguous across providers. This isn't a bug today, but it's the kind of behavior that surprises users when they reorder a config file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/providers/src/claude/model-registry.ts` around lines 184 - 206, The
resolve() logic currently returns the first match across
Object.entries(this.providers) in steps 2–4 (case where code checks
provider.models for id/name and calls buildResult), which makes cross-provider
collisions depend on insertion order; update resolve() to detect ambiguous
matches across providers for the same model id or name (collect all matching
providers when searching in the loops), and if more than one provider matches
return a deterministic error or warning (or require scoping) instead of
returning the first winner; alternatively, add clear JSDoc above resolve()
explaining insertion-order precedence if you prefer to keep current behavior.
packages/docs-web/src/content/docs/reference/configuration.md (1)

99-141: 💤 Low value

Consider documenting authToken and credential precedence.

The JSON example shows only apiKey, but the field-mapping table at Lines 134–139 also lists authToken (→ ANTHROPIC_AUTH_TOKEN). New users will likely copy the example and miss that bearer-token gateways need authToken instead. A one-line note (or a second example block mirroring the local provider in claude-models.json.example) would close that gap. It would also help to clarify what happens when both apiKey and authToken are configured for the same provider, since the registry currently injects both env vars.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/docs-web/src/content/docs/reference/configuration.md` around lines
99 - 141, Add a one-line note after the claude-models.json example explaining
that bearer-token gateways should use the authToken field (mapped to
ANTHROPIC_AUTH_TOKEN) instead of apiKey, include a second small JSON snippet
showing authToken used in place of apiKey (matching the existing example
format), and clarify credential precedence by stating that the registry may
inject both ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN but the provider should
prefer authToken when both are present.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/providers/src/claude/model-registry.ts`:
- Around line 118-122: The loop in model-registry.ts that validates providers
currently accepts empty strings for baseUrl and credentials; tighten the checks
in the provider validation (the for (const [name, provider] ...) block) so
provider.baseUrl must be a non-empty string (e.g., typeof provider.baseUrl ===
'string' && provider.baseUrl.trim() !== '') and require at least one non-empty
credential by checking that either provider.apiKey or provider.authToken is a
non-empty string (typeof ... === 'string' && ... .trim() !== ''); keep the
existing Array.isArray(provider.models) check and ensure failing these tighter
checks causes the entry to be skipped/produce a loadError so buildResult won't
receive empty baseUrl or credentials.
- Around line 223-227: The provider header handling in load() must validate
provider.headers entries and skip/reject ones containing control chars that will
break the "Name: Value" format: ensure header names do not contain ':' or '\n'
or '\r' and header values do not contain '\n' or '\r'; when such invalid entries
are found, do not include them in env.ANTHROPIC_CUSTOM_HEADERS and record a
descriptive loadError breadcrumb (e.g. referencing the provider id/name and
which header key was invalid). Update the logic around provider.headers ->
env.ANTHROPIC_CUSTOM_HEADERS in model-registry.ts (the load() path that maps
Object.entries(provider.headers)) to perform these checks, skip bad entries, and
set loadError accordingly.

---

Nitpick comments:
In `@packages/docs-web/src/content/docs/getting-started/ai-assistants.md`:
- Around line 132-170: Add a one-line security callout to the "Custom
Claude-Compatible Providers" section warning that the example secrets in
~/.archon/claude-models.json are stored in plaintext and persist across
sessions; instruct users to restrict file permissions (e.g., chmod 600) and
never commit the file or keys to version control, and place this sentence near
the JSON example or immediately before the paragraph describing
apiKey/authToken/baseUrl so it's clearly associated with claude-models.json and
the assistants.claude.model configuration.

In `@packages/docs-web/src/content/docs/reference/configuration.md`:
- Around line 99-141: Add a one-line note after the claude-models.json example
explaining that bearer-token gateways should use the authToken field (mapped to
ANTHROPIC_AUTH_TOKEN) instead of apiKey, include a second small JSON snippet
showing authToken used in place of apiKey (matching the existing example
format), and clarify credential precedence by stating that the registry may
inject both ANTHROPIC_API_KEY and ANTHROPIC_AUTH_TOKEN but the provider should
prefer authToken when both are present.

In `@packages/providers/src/claude/model-registry.ts`:
- Around line 184-206: The resolve() logic currently returns the first match
across Object.entries(this.providers) in steps 2–4 (case where code checks
provider.models for id/name and calls buildResult), which makes cross-provider
collisions depend on insertion order; update resolve() to detect ambiguous
matches across providers for the same model id or name (collect all matching
providers when searching in the loops), and if more than one provider matches
return a deterministic error or warning (or require scoping) instead of
returning the first winner; alternatively, add clear JSDoc above resolve()
explaining insertion-order precedence if you prefer to keep current behavior.

In `@packages/providers/src/claude/provider.ts`:
- Around line 941-971: The code instantiates a new ClaudeModelRegistry inside
the request path (new ClaudeModelRegistry()) causing synchronous disk reads on
every sendQuery call; fix it by introducing a module-scoped lazy singleton
accessor (e.g., getClaudeModelRegistry()) that returns a cached
ClaudeModelRegistry instance and only re-creates it when the backing file
changes (use fs.statSync mtime check for hot-reload) and then replace
occurrences of new ClaudeModelRegistry() in provider.ts with calls to
getClaudeModelRegistry(); keep using registry.getError() and
registry.resolve(rawModel) unchanged so logging and registryEnv behavior remain
the same.
- Around line 949-952: The registry parse errors are only logged
(registry.getError() -> getLog().warn) so callers don't see them; update the
code path that handles registryError to also emit a system-facing chunk/message
like the nodeConfigWarnings pattern (see nodeConfigWarnings handling around
nodeConfigWarnings usage) — e.g., when registryError exists, in addition to
getLog().warn({ error: registryError }, 'claude.model_registry_load_error'),
create/yield a system chunk or push a system message containing "⚠️ Claude model
registry failed to load: {registryError.message}" so the caller sees the
registry parse error before any downstream "unknown model" SDK error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 90a9d59e-8550-4441-837f-7980e9f88c1a

📥 Commits

Reviewing files that changed from the base of the PR and between 0ec7441 and 917bd7d.

📒 Files selected for processing (8)
  • packages/docs-web/src/content/docs/getting-started/ai-assistants.md
  • packages/docs-web/src/content/docs/reference/configuration.md
  • packages/providers/package.json
  • packages/providers/src/claude/claude-models.json.example
  • packages/providers/src/claude/model-registry.test.ts
  • packages/providers/src/claude/model-registry.ts
  • packages/providers/src/claude/provider.test.ts
  • packages/providers/src/claude/provider.ts

Comment thread packages/providers/src/claude/model-registry.ts Outdated
Comment thread packages/providers/src/claude/model-registry.ts Outdated
@guanghuang guanghuang force-pushed the feat/claude-custom-model-registry branch from 917bd7d to 4e5b861 Compare May 5, 2026 14:37
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/providers/src/claude/model-registry.ts`:
- Around line 147-159: The provider validation currently only records messages
for some failures; update the validation in model-registry.ts so every skipped
provider shape appends a clear breadcrumb to validationMessages — specifically
add messages when "provider" is missing or not an object (use the existing
"name" to identify the entry) and when "provider.models" is not an array,
matching the style of the existing checks for baseUrl/apiKey/authToken; keep the
existing continue behavior but ensure each early-continue branch (the provider
object check and the Array.isArray(provider.models) check) pushes a descriptive
`"Provider \"${name}\" skipped: <reason>"` entry into validationMessages so all
ignored entries are diagnosable.
- Around line 131-136: The current validation allows providers to be null so
Object.entries(config.providers) can throw; tighten the guard around
parsed/(ClaudeModelsConfig).providers to require a non-null plain object and
reject arrays (e.g. check parsed && typeof parsed === 'object' && (parsed as
ClaudeModelsConfig).providers !== null && typeof (parsed as
ClaudeModelsConfig).providers === 'object' && !Array.isArray((parsed as
ClaudeModelsConfig).providers)); update the same check before any use of
Object.entries(config.providers) or in the load path that reads config.providers
to ensure you only iterate when providers is a real object (or use a small
isPlainObject utility and call that).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b0c2b3d7-9f53-44c3-bae3-98061e30f21c

📥 Commits

Reviewing files that changed from the base of the PR and between 917bd7d and 4e5b861.

📒 Files selected for processing (8)
  • packages/docs-web/src/content/docs/getting-started/ai-assistants.md
  • packages/docs-web/src/content/docs/reference/configuration.md
  • packages/providers/package.json
  • packages/providers/src/claude/claude-models.json.example
  • packages/providers/src/claude/model-registry.test.ts
  • packages/providers/src/claude/model-registry.ts
  • packages/providers/src/claude/provider.test.ts
  • packages/providers/src/claude/provider.ts
✅ Files skipped from review due to trivial changes (4)
  • packages/providers/src/claude/claude-models.json.example
  • packages/providers/package.json
  • packages/docs-web/src/content/docs/reference/configuration.md
  • packages/docs-web/src/content/docs/getting-started/ai-assistants.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • packages/providers/src/claude/provider.ts
  • packages/providers/src/claude/provider.test.ts

Comment thread packages/providers/src/claude/model-registry.ts Outdated
Comment on lines +147 to +159
if (!provider || typeof provider !== 'object') continue;
if (!isNonEmptyString(provider.baseUrl)) {
validationMessages.push(`Provider "${name}" skipped: baseUrl must be a non-empty string`);
continue;
}
if (!isNonEmptyString(provider.apiKey) && !isNonEmptyString(provider.authToken)) {
validationMessages.push(
`Provider "${name}" skipped: apiKey or authToken must be a non-empty string`
);
continue;
}
if (!Array.isArray(provider.models)) continue;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Record validation breadcrumbs for all skipped invalid providers.

Some invalid provider shapes are skipped silently (provider non-object, models non-array), while other invalid cases emit validationMessages. Make this consistent so users can diagnose why entries were ignored.

As per coding guidelines, “Prefer throwing early with a clear error for unsupported or unsafe states; never silently swallow errors or broaden permissions; document intentional fallback behavior with a comment”.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/providers/src/claude/model-registry.ts` around lines 147 - 159, The
provider validation currently only records messages for some failures; update
the validation in model-registry.ts so every skipped provider shape appends a
clear breadcrumb to validationMessages — specifically add messages when
"provider" is missing or not an object (use the existing "name" to identify the
entry) and when "provider.models" is not an array, matching the style of the
existing checks for baseUrl/apiKey/authToken; keep the existing continue
behavior but ensure each early-continue branch (the provider object check and
the Array.isArray(provider.models) check) pushes a descriptive `"Provider
\"${name}\" skipped: <reason>"` entry into validationMessages so all ignored
entries are diagnosable.

@guanghuang guanghuang force-pushed the feat/claude-custom-model-registry branch from 4e5b861 to 0af28fa Compare May 5, 2026 14:49
@guanghuang guanghuang force-pushed the feat/claude-custom-model-registry branch from 0af28fa to 05d0525 Compare May 5, 2026 19:38
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/providers/src/claude/model-registry.ts`:
- Around line 131-133: ClaudeModelRegistry currently does synchronous file I/O
in its constructor via load(), causing readFileSync to run per-request; change
this to a cached module-level singleton factory (e.g., add getModelRegistry())
that reads the file once (or on file-change/TTL) and returns the same
ClaudeModelRegistry instance, update provider.ts to call getModelRegistry()
instead of new ClaudeModelRegistry(), and move the readFileSync logic out of the
constructor (keep load() for refreshes) so requests use the cached instance
rather than performing synchronous disk reads on every instantiation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: abb5c3c9-308b-4114-89a4-cad5ff696e02

📥 Commits

Reviewing files that changed from the base of the PR and between 0af28fa and 05d0525.

📒 Files selected for processing (8)
  • packages/docs-web/src/content/docs/getting-started/ai-assistants.md
  • packages/docs-web/src/content/docs/reference/configuration.md
  • packages/providers/package.json
  • packages/providers/src/claude/claude-models.json.example
  • packages/providers/src/claude/model-registry.test.ts
  • packages/providers/src/claude/model-registry.ts
  • packages/providers/src/claude/provider.test.ts
  • packages/providers/src/claude/provider.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/providers/package.json
  • packages/providers/src/claude/claude-models.json.example
  • packages/docs-web/src/content/docs/reference/configuration.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/docs-web/src/content/docs/getting-started/ai-assistants.md
  • packages/providers/src/claude/provider.test.ts
  • packages/providers/src/claude/provider.ts

Comment on lines +131 to +133
constructor() {
this.load();
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Synchronous file I/O on every request blocks the event loop.

ClaudeModelRegistry is instantiated per request in provider.ts (line 948), meaning readFileSync is called synchronously on every Claude inference request. This blocks the event loop for the duration of the disk read and adds latency to every request.

Consider a module-level singleton (or a cached instance with a short TTL/file-watch invalidation) so the file is read at most once (or on change):

🛠️ Suggested module-level singleton approach
+let _registrySingleton: ClaudeModelRegistry | undefined;
+
+export function getModelRegistry(): ClaudeModelRegistry {
+  if (!_registrySingleton) {
+    _registrySingleton = new ClaudeModelRegistry();
+  }
+  return _registrySingleton;
+}

 export class ClaudeModelRegistry {

Then in provider.ts, replace new ClaudeModelRegistry() with getModelRegistry().

Also applies to: 141-142

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/providers/src/claude/model-registry.ts` around lines 131 - 133,
ClaudeModelRegistry currently does synchronous file I/O in its constructor via
load(), causing readFileSync to run per-request; change this to a cached
module-level singleton factory (e.g., add getModelRegistry()) that reads the
file once (or on file-change/TTL) and returns the same ClaudeModelRegistry
instance, update provider.ts to call getModelRegistry() instead of new
ClaudeModelRegistry(), and move the readFileSync logic out of the constructor
(keep load() for refreshes) so requests use the cached instance rather than
performing synchronous disk reads on every instantiation.

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.

Support custom Claude provider model aliases

1 participant