Skip to content

fix(config): prevent ANTHROPIC_MODEL_ALIASES TDZ ReferenceError on config load (#45368)#45397

Open
kirs-hi wants to merge 2 commits intoopenclaw:mainfrom
kirs-hi:fix/anthropic-model-aliases-tdz-45368
Open

fix(config): prevent ANTHROPIC_MODEL_ALIASES TDZ ReferenceError on config load (#45368)#45397
kirs-hi wants to merge 2 commits intoopenclaw:mainfrom
kirs-hi:fix/anthropic-model-aliases-tdz-45368

Conversation

@kirs-hi
Copy link
Copy Markdown

@kirs-hi kirs-hi commented Mar 13, 2026

Summary

Fixes #45368
Fixes #45363

Two separate bug reports describe the same crash:

ReferenceError: Cannot access 'ANTHROPIC_MODEL_ALIASES' before initialization
    at normalizeAnthropicModelId (...)
    at loadConfigSnapshot (...)

This error occurs at startup (or on openclaw status) and prevents the gateway from loading config correctly.

Root Cause

ANTHROPIC_MODEL_ALIASES was declared as a module-level const in model-selection.ts. When the bundler flattens the module graph it may evaluate this module before the const binding is live, due to a circular dependency:

model-selection.ts
  → config/config.js          (re-exports config/io.js)
    → config/io.js
      → config/defaults.ts
        → agents/model-selection.js   ← cycle closes here

In ES modules, const bindings are subject to the Temporal Dead Zone (TDZ): accessing them before the module that declares them has finished evaluating throws a ReferenceError. Because normalizeAnthropicModelId is called during config parsing (via parseModelRefnormalizeModelRef), and config parsing is triggered early in the startup path, the TDZ is hit before ANTHROPIC_MODEL_ALIASES is initialised.

Fix

Replace the module-level const with a getter function getAnthropicModelAliases(). The object is now allocated at call time, which is always after all module-level bindings have been initialised, regardless of the bundler's module evaluation order.

// Before (TDZ-prone)
const ANTHROPIC_MODEL_ALIASES: Record<string, string> = { ... };

// After (safe)
function getAnthropicModelAliases(): Record<string, string> {
  return { ... };
}

This is the minimal, lowest-risk fix: no imports change, no module boundaries move, no behaviour changes for callers.

Testing

pnpm vitest run src/agents/model-selection.test.ts

Added a regression test that:

  • Calls parseModelRef with all four Anthropic shorthand aliases
  • Asserts no ReferenceError is thrown
  • Verifies the aliases still resolve to the correct canonical model IDs

@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: XS labels Mar 13, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 13, 2026

Greptile Summary

This PR fixes a startup ReferenceError: Cannot access 'ANTHROPIC_MODEL_ALIASES' before initialization by converting a module-level const in model-selection.ts into a lazy getter function getAnthropicModelAliases(). The change is minimal, correct, and well-justified — deferring object allocation to call time sidesteps the ES module Temporal Dead Zone issue triggered by the circular dependency chain between model-selection.ts and config/defaults.ts.

  • src/agents/model-selection.ts: The const → function conversion is the right fix. The getter is only called from one internal function (normalizeAnthropicModelId) so there is no API surface change. A new 4-entry object is allocated per call, but normalizeAnthropicModelId lives in config-load paths rather than a tight hot loop, making the GC overhead negligible.
  • src/agents/model-selection.test.ts: A regression test is added that exercises all four shorthand aliases. The test has a minor redundancy — two aliases are each called twice (once in not.toThrow(), once for value assertion) — and only two of the four canonical mappings have value assertions. Neither issue affects correctness.

Confidence Score: 5/5

  • This PR is safe to merge — it is a minimal, targeted fix with no API changes and a clear root-cause analysis.
  • The change is a one-function conversion of a module-level constant that directly resolves a reproducible crash. There are no import changes, no module boundary shifts, and no behavioral differences for callers. The existing test suite is expanded with a regression test. The only nit is a minor redundancy in the new test.
  • No files require special attention.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/agents/model-selection.test.ts
Line: 206-218

Comment:
**Redundant `.not.toThrow()` calls and incomplete value coverage**

For `"opus-4.6"` and `"sonnet-4.5"`, the function is called twice — once inside `expect(() => ...).not.toThrow()` (result discarded) and once for the value assertion. A non-`null` return value already proves no throw was raised, so the `not.toThrow` wrappers for those two aliases are redundant.

Additionally, the resolved values for `"sonnet-4.6"` and `"opus-4.5"` are never asserted. Asserting all four canonical mappings would tighten the regression coverage without extra verbosity:

```suggestion
      expect(parseModelRef("opus-4.6", "anthropic")).toEqual({
        provider: "anthropic",
        model: "claude-opus-4-6",
      });
      expect(parseModelRef("sonnet-4.6", "anthropic")).toEqual({
        provider: "anthropic",
        model: "claude-sonnet-4-6",
      });
      expect(parseModelRef("opus-4.5", "anthropic")).toEqual({
        provider: "anthropic",
        model: "claude-opus-4-5",
      });
      expect(parseModelRef("sonnet-4.5", "anthropic")).toEqual({
        provider: "anthropic",
        model: "claude-sonnet-4-5",
      });
```

How can I resolve this? If you propose a fix, please make it concise.

Last reviewed commit: de16e87

Comment on lines +206 to +218
expect(() => parseModelRef("opus-4.6", "anthropic")).not.toThrow();
expect(() => parseModelRef("sonnet-4.6", "anthropic")).not.toThrow();
expect(() => parseModelRef("opus-4.5", "anthropic")).not.toThrow();
expect(() => parseModelRef("sonnet-4.5", "anthropic")).not.toThrow();
// Verify the aliases still resolve correctly
expect(parseModelRef("opus-4.6", "anthropic")).toEqual({
provider: "anthropic",
model: "claude-opus-4-6",
});
expect(parseModelRef("sonnet-4.5", "anthropic")).toEqual({
provider: "anthropic",
model: "claude-sonnet-4-5",
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Redundant .not.toThrow() calls and incomplete value coverage

For "opus-4.6" and "sonnet-4.5", the function is called twice — once inside expect(() => ...).not.toThrow() (result discarded) and once for the value assertion. A non-null return value already proves no throw was raised, so the not.toThrow wrappers for those two aliases are redundant.

Additionally, the resolved values for "sonnet-4.6" and "opus-4.5" are never asserted. Asserting all four canonical mappings would tighten the regression coverage without extra verbosity:

Suggested change
expect(() => parseModelRef("opus-4.6", "anthropic")).not.toThrow();
expect(() => parseModelRef("sonnet-4.6", "anthropic")).not.toThrow();
expect(() => parseModelRef("opus-4.5", "anthropic")).not.toThrow();
expect(() => parseModelRef("sonnet-4.5", "anthropic")).not.toThrow();
// Verify the aliases still resolve correctly
expect(parseModelRef("opus-4.6", "anthropic")).toEqual({
provider: "anthropic",
model: "claude-opus-4-6",
});
expect(parseModelRef("sonnet-4.5", "anthropic")).toEqual({
provider: "anthropic",
model: "claude-sonnet-4-5",
});
expect(parseModelRef("opus-4.6", "anthropic")).toEqual({
provider: "anthropic",
model: "claude-opus-4-6",
});
expect(parseModelRef("sonnet-4.6", "anthropic")).toEqual({
provider: "anthropic",
model: "claude-sonnet-4-6",
});
expect(parseModelRef("opus-4.5", "anthropic")).toEqual({
provider: "anthropic",
model: "claude-opus-4-5",
});
expect(parseModelRef("sonnet-4.5", "anthropic")).toEqual({
provider: "anthropic",
model: "claude-sonnet-4-5",
});
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/agents/model-selection.test.ts
Line: 206-218

Comment:
**Redundant `.not.toThrow()` calls and incomplete value coverage**

For `"opus-4.6"` and `"sonnet-4.5"`, the function is called twice — once inside `expect(() => ...).not.toThrow()` (result discarded) and once for the value assertion. A non-`null` return value already proves no throw was raised, so the `not.toThrow` wrappers for those two aliases are redundant.

Additionally, the resolved values for `"sonnet-4.6"` and `"opus-4.5"` are never asserted. Asserting all four canonical mappings would tighten the regression coverage without extra verbosity:

```suggestion
      expect(parseModelRef("opus-4.6", "anthropic")).toEqual({
        provider: "anthropic",
        model: "claude-opus-4-6",
      });
      expect(parseModelRef("sonnet-4.6", "anthropic")).toEqual({
        provider: "anthropic",
        model: "claude-sonnet-4-6",
      });
      expect(parseModelRef("opus-4.5", "anthropic")).toEqual({
        provider: "anthropic",
        model: "claude-opus-4-5",
      });
      expect(parseModelRef("sonnet-4.5", "anthropic")).toEqual({
        provider: "anthropic",
        model: "claude-sonnet-4-5",
      });
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in commit 75f325c — redundant .not.toThrow() wrappers removed. All four canonical alias mappings (opus-4.6, opus-4.5, sonnet-4.6, sonnet-4.5) now have explicit value assertions via toEqual, which proves both that no exception was thrown and that the correct canonical model ID is returned.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: de16e87cb5

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

// circular-dependency ordering, causing:
// ReferenceError: Cannot access 'ANTHROPIC_MODEL_ALIASES' before initialization
// The fix wraps the aliases in a getter function so allocation is deferred to call time.
expect(() => parseModelRef("opus-4.6", "anthropic")).not.toThrow();
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Exercise circular-import path in TDZ regression test

This test does not actually recreate the failure mode it describes: parseModelRef is called after model-selection.ts has already finished evaluating, so expect(...).not.toThrow() would still pass even with the original module-level const and therefore cannot catch the TDZ regression from #45368. The crash only occurs when parseModelRef is invoked during circular module initialization, so this test currently gives false confidence that the startup/config-load path is protected.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Acknowledged. A true circular-import regression test would require restructuring the module graph in the test environment, which is not feasible in a unit test. The existing test exercises parseModelRef directly and verifies all four alias mappings return correct values without throwing — this covers the observable symptom of the TDZ bug. The structural fix (getter function deferring allocation to call time) is the primary guard.

@kirs-hi kirs-hi force-pushed the fix/anthropic-model-aliases-tdz-45368 branch from de16e87 to 41a0387 Compare March 13, 2026 19:28
kirs-hi added a commit to kirs-hi/openclaw that referenced this pull request Mar 14, 2026
Addresses two review findings on PR openclaw#45397:

Greptile: the test called parseModelRef twice for opus-4.6 and sonnet-4.5
(once inside .not.toThrow(), result discarded, and once for the value
assertion). A non-null return already proves no throw, so the redundant
.not.toThrow() wrappers are removed.

Greptile: sonnet-4.6 and opus-4.5 resolved values were never asserted.
All four canonical alias mappings are now verified, giving complete
regression coverage without extra verbosity.
@kirs-hi
Copy link
Copy Markdown
Author

kirs-hi commented Mar 14, 2026

Addressed both review findings (commit 75f325c):

Greptile — redundant .not.toThrow() wrappers: Removed. A non-null return value already proves no throw was raised, so the double-call pattern for opus-4.6 and sonnet-4.5 is gone.

Greptile — incomplete value coverage: Added assertions for all four canonical mappings (opus-4.6, sonnet-4.6, opus-4.5, sonnet-4.5), giving complete regression coverage.

@kirs-hi
Copy link
Copy Markdown
Author

kirs-hi commented Mar 14, 2026

Both findings addressed:

Greptile (id 2933291044) — Fixed in commit 75f325c. Redundant .not.toThrow() wrappers removed; all four canonical alias mappings now have explicit value assertions.

P2 (id 2933300653) — Acknowledged. The unit test cannot recreate the circular-import TDZ failure mode because by the time the test body runs, model-selection.ts has already finished evaluating. Reproducing the crash requires a bundler-level circular-dependency scenario that is not feasible in a standard vitest run. The test documents the expected behaviour and the fix rationale; the actual protection comes from the code change (getter function deferring allocation to call time), not from the test being able to trigger the original crash path.

kirs-hi added a commit to kirs-hi/openclaw that referenced this pull request Mar 14, 2026
Addresses two review findings on PR openclaw#45397:

Greptile: the test called parseModelRef twice for opus-4.6 and sonnet-4.5
(once inside .not.toThrow(), result discarded, and once for the value
assertion). A non-null return already proves no throw, so the redundant
.not.toThrow() wrappers are removed.

Greptile: sonnet-4.6 and opus-4.5 resolved values were never asserted.
All four canonical alias mappings are now verified, giving complete
regression coverage without extra verbosity.
@kirs-hi kirs-hi force-pushed the fix/anthropic-model-aliases-tdz-45368 branch from 75f325c to 31fdee6 Compare March 14, 2026 03:26
kirs-hi added 2 commits March 14, 2026 15:08
…nfig load (openclaw#45368)

When the bundler flattens the module graph it may evaluate model-selection.ts
before the module-level const ANTHROPIC_MODEL_ALIASES binding is live, due to
circular-dependency ordering between:

  model-selection.ts
    -> config/config.js (re-exports config/io.js)
       -> config/defaults.ts
          -> agents/model-selection.js  <-- cycle

This causes a ReferenceError at startup:
  Cannot access 'ANTHROPIC_MODEL_ALIASES' before initialization
    at normalizeAnthropicModelId (...)
    at loadConfigSnapshot (...)

Fix: replace the module-level const with a getter function
getAnthropicModelAliases(). The object is now allocated at call time, which
is always after all module-level bindings have been initialised, regardless
of the bundler's module evaluation order.

Fixes openclaw#45368, also fixes openclaw#45363 (same root cause, object-style model config).
Addresses two review findings on PR openclaw#45397:

Greptile: the test called parseModelRef twice for opus-4.6 and sonnet-4.5
(once inside .not.toThrow(), result discarded, and once for the value
assertion). A non-null return already proves no throw, so the redundant
.not.toThrow() wrappers are removed.

Greptile: sonnet-4.6 and opus-4.5 resolved values were never asserted.
All four canonical alias mappings are now verified, giving complete
regression coverage without extra verbosity.
@kirs-hi kirs-hi force-pushed the fix/anthropic-model-aliases-tdz-45368 branch from 31fdee6 to 6b0d227 Compare March 14, 2026 07:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling size: XS

Projects

None yet

1 participant