fix(models): route all Claude aliases through the protoLabs gateway#3671
Conversation
The gateway-issued API key only allows `protolabs/*` tier names. Every piece of routing now consistently resolves Claude aliases to one of the three gateway tiers: claude-haiku / haiku → protolabs/fast claude-sonnet / sonnet → protolabs/smart claude-opus / opus → protolabs/reasoning This was a half-finished cutover — DEFAULT_MODELS and DEFAULT_PHASE_MODELS already pointed at gateway tiers, but CLAUDE_CANONICAL_MAP / CLAUDE_MODEL_MAP still resolved to versioned Anthropic strings (claude-sonnet-4-6 etc.). Any install that had stored those legacy strings in data/settings.json (every install from before the cutover) got a hard 401 on the first agent turn: [API Error: 401 key not allowed to access model. This key can only access models=['protolabs/smart', ...]. Tried to access claude-sonnet-4-6] Changes: - libs/types/src/model.ts: CLAUDE_CANONICAL_MAP and CLAUDE_MODEL_MAP values updated to protolabs/* tiers. New LEGACY_CLAUDE_FULL_MODEL_MAP covers persisted versioned strings (claude-sonnet-4-6 etc.) so any stored settings get rewritten on next load. - libs/types/src/model-migration.ts: migrateModelId rewrites legacy canonical IDs and full versioned strings to gateway tiers. Short aliases also map directly to tiers (no claude-* intermediate). - libs/types/src/global-settings.ts: SETTINGS_VERSION bumped 6 → 7 to signal one-shot migration on next load. SettingsService already re-runs migratePhaseModels every load, so no new migration code is needed — the bumped version is for telemetry / future schema fences. - libs/types/src/index.ts: export LEGACY_CLAUDE_FULL_MODEL_MAP. Tests: - New libs/types/tests/model-migration-gateway.test.ts: 22 tests covering canonical IDs, short aliases, versioned legacy strings, protolabs/* passthrough, and unrecognized-string passthrough. - Updated 14 existing assertions across model-resolver, execution-service, settings-service, and sdk-options test files to expect protolabs/* outputs instead of versioned claude-* strings. Per the greenfield-first rule, there is no direct-Anthropic path anymore — these assertions had become aspirational rather than describing real behavior. Full server suite passes: 3397/3397. Packages: 1152/1152. Typecheck: clean. Closes #3661
Code Review — ? finding(s)
protoLabs Code Review Report
No findings recorded. |
There was a problem hiding this comment.
QA Audit — PR #3671 | fix(models): route all Claude aliases through the protoLabs gateway
VERDICT: WARN
CI Status
- test: queued
- checks: in_progress
- build: in_progress
Diff Review
- Updates model resolution maps to route Claude aliases (
haiku,sonnet,opus) to protoLabs gateway tiers (protolabs/fast,protolabs/smart,protolabs/reasoning). - Updates
migrateModelIdto rewrite legacy versioned strings to gateway tiers. - Bumps
SETTINGS_VERSIONto 7. - Updates test suites to assert on the new gateway tier strings.
Observations
- LOW: CI checks are currently in progress or queued. Review is based on the provided diff and author's local test results.
|
I have submitted a COMMENT review on #3671. |
📝 WalkthroughWalkthroughThis PR migrates Claude-family model identifiers to route through a Protolabs gateway tier system. Model aliases ( ChangesClaude Model Gateway Tier Migration
Estimated Code Review Effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly Related Issues
Possibly Related PRs
Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint install failed: one or more packages not found in the registry. Comment |
There was a problem hiding this comment.
This PR fixes the issue reported in #3678 by routing Claude aliases through the protoLabs gateway, which bypasses the need for a direct Anthropic API key. Approving to unblock bug triage.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
libs/types/tests/model-migration-gateway.test.ts (1)
10-15: ⚡ Quick winUse
@protolabsai/*imports instead of relative../src/*imports.Line 10 and Lines 11-15 should import shared types APIs through the package namespace to keep tests aligned with the public/shared contract.
♻️ Suggested import update
-import { migrateModelId } from '../src/model-migration.js'; -import { - CLAUDE_CANONICAL_MAP, - CLAUDE_MODEL_MAP, - LEGACY_CLAUDE_FULL_MODEL_MAP, -} from '../src/model.js'; +import { + migrateModelId, + CLAUDE_CANONICAL_MAP, + CLAUDE_MODEL_MAP, + LEGACY_CLAUDE_FULL_MODEL_MAP, +} from '`@protolabsai/types`';As per coding guidelines: "Import from shared packages using the
@protolabsai/* namespace rather than importing from old paths or relative paths within the codebase."🤖 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 `@libs/types/tests/model-migration-gateway.test.ts` around lines 10 - 15, Replace the relative imports in the test with the public package namespace: import migrateModelId, CLAUDE_CANONICAL_MAP, CLAUDE_MODEL_MAP, and LEGACY_CLAUDE_FULL_MODEL_MAP from the appropriate `@protolabsai/`* package(s) instead of '../src/*'; update the import statements that reference migrateModelId and the three map constants so the test consumes the shared public API (e.g., import { migrateModelId } from '`@protolabsai/`<pkg>' and import { CLAUDE_CANONICAL_MAP, CLAUDE_MODEL_MAP, LEGACY_CLAUDE_FULL_MODEL_MAP } from '`@protolabsai/`<pkg>') to align with the package contract.
🤖 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 `@libs/types/src/model-migration.ts`:
- Around line 79-98: The map lookups in migrateModelId and isLegacyClaudeAlias
use the `in` operator (e.g. `legacyId in LEGACY_CLAUDE_FULL_MODEL_MAP`,
`legacyId in CLAUDE_CANONICAL_MAP`, and `id in LEGACY_CLAUDE_ALIAS_MAP`) which
can match prototype keys like "__proto__"; change them to own-property checks
using either `Object.prototype.hasOwnProperty.call(OBJ, key)` or
`Object.hasOwn(OBJ, key)` so only actual map keys are matched, and ensure the
code paths in migrateModelId and isLegacyClaudeAlias then use the returned
string values directly (no prototype values).
---
Nitpick comments:
In `@libs/types/tests/model-migration-gateway.test.ts`:
- Around line 10-15: Replace the relative imports in the test with the public
package namespace: import migrateModelId, CLAUDE_CANONICAL_MAP,
CLAUDE_MODEL_MAP, and LEGACY_CLAUDE_FULL_MODEL_MAP from the appropriate
`@protolabsai/`* package(s) instead of '../src/*'; update the import statements
that reference migrateModelId and the three map constants so the test consumes
the shared public API (e.g., import { migrateModelId } from '`@protolabsai/`<pkg>'
and import { CLAUDE_CANONICAL_MAP, CLAUDE_MODEL_MAP,
LEGACY_CLAUDE_FULL_MODEL_MAP } from '`@protolabsai/`<pkg>') to align with the
package contract.
🪄 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: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: a12f932a-a988-459d-9eb4-53f5d950a993
📒 Files selected for processing (10)
apps/server/tests/unit/lib/model-resolver.test.tsapps/server/tests/unit/lib/sdk-options.test.tsapps/server/tests/unit/services/execution-service.test.tsapps/server/tests/unit/services/settings-service.test.tslibs/model-resolver/tests/resolver.test.tslibs/types/src/global-settings.tslibs/types/src/index.tslibs/types/src/model-migration.tslibs/types/src/model.tslibs/types/tests/model-migration-gateway.test.ts
| // Full versioned Claude model strings from before the gateway cutover | ||
| // (e.g. 'claude-sonnet-4-6'). Rewrite to the gateway tier so the | ||
| // gateway-only API key accepts them. | ||
| if (legacyId in LEGACY_CLAUDE_FULL_MODEL_MAP) { | ||
| return LEGACY_CLAUDE_FULL_MODEL_MAP[legacyId]; | ||
| } | ||
|
|
||
| // Already has claude- prefix and is in canonical map. CLAUDE_CANONICAL_MAP | ||
| // now points at gateway tiers — return the mapped tier, not the prefixed | ||
| // alias, so any callsite that stored a stale canonical ID gets the right | ||
| // resolution on the next load. | ||
| if (legacyId.startsWith('claude-') && legacyId in CLAUDE_CANONICAL_MAP) { | ||
| return legacyId; | ||
| return CLAUDE_CANONICAL_MAP[legacyId as keyof typeof CLAUDE_CANONICAL_MAP]; | ||
| } | ||
|
|
||
| // Legacy Claude alias (short name) | ||
| // Legacy Claude alias (short name) — map directly to the gateway tier. | ||
| if (isLegacyClaudeAlias(legacyId)) { | ||
| return LEGACY_CLAUDE_ALIAS_MAP[legacyId]; | ||
| const canonical = LEGACY_CLAUDE_ALIAS_MAP[legacyId]; | ||
| return CLAUDE_CANONICAL_MAP[canonical]; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate migrateModelId and the relevant lines
echo "==> model-migration.ts: migrateModelId section"
FILE="libs/types/src/model-migration.ts"
test -f "$FILE"
rg -n "function migrateModelId|migrateModelId" "$FILE" || true
# Print surrounding lines near the in-operators mentioned
rg -n "in LEGACY_CLAUDE_FULL_MODEL_MAP|in CLAUDE_CANONICAL_MAP|LEGACY_CLAUDE_ALIAS_MAP" "$FILE" || true
# Show ~120 lines around first match
LINE1=$(rg -n "in LEGACY_CLAUDE_FULL_MODEL_MAP" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${LINE1:-}" ]; then
START=$((LINE1-15)); END=$((LINE1+25))
sed -n "${START},${END}p" "$FILE" | nl -ba -w2 -s: | head -n 120
fi
echo
echo "==> model.ts: map declarations"
MODEL_FILE="libs/types/src/model.ts"
test -f "$MODEL_FILE"
rg -n "LEGACY_CLAUDE_FULL_MODEL_MAP|CLAUDE_CANONICAL_MAP|LEGACY_CLAUDE_ALIAS_MAP" "$MODEL_FILE" || true
for sym in LEGACY_CLAUDE_FULL_MODEL_MAP CLAUDE_CANONICAL_MAP LEGACY_CLAUDE_ALIAS_MAP; do
L=$(rg -n "${sym}" "$MODEL_FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${L:-}" ]; then
START=$((L-10)); END=$((L+40))
echo "-- ${sym} excerpt"
sed -n "${START},${END}p" "$MODEL_FILE" | nl -ba -w2 -s: | head -n 120
fi
done
echo
echo "==> model-migration.ts: helper isLegacyClaudeAlias"
rg -n "isLegacyClaudeAlias" "$FILE" || true
L2=$(rg -n "isLegacyClaudeAlias" "$FILE" | head -n1 | cut -d: -f1 || true)
if [ -n "${L2:-}" ]; then
START=$((L2-10)); END=$((L2+40))
sed -n "${START},${END}p" "$FILE" | nl -ba -w2 -s: | head -n 120
fiRepository: protoLabsAI/protoMaker
Length of output: 614
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE_MIG="libs/types/src/model-migration.ts"
FILE_MODEL="libs/types/src/model.ts"
echo "==> model-migration.ts: helper isLegacyClaudeAlias (context)"
L2=$(rg -n "function isLegacyClaudeAlias" "$FILE_MIG" | head -n1 | cut -d: -f1 || true)
if [ -n "${L2:-}" ]; then
START=$((L2-20)); END=$((L2+40))
sed -n "${START},${END}p" "$FILE_MIG"
fi
echo
echo "==> model-migration.ts: migrateModelId block (context around in-operators)"
for pat in "in LEGACY_CLAUDE_FULL_MODEL_MAP" "in CLAUDE_CANONICAL_MAP" "LEGACY_CLAUDE_ALIAS_MAP\\[legacyId\\]"; do
L=$(rg -n "$pat" "$FILE_MIG" | head -n1 | cut -d: -f1 || true)
if [ -n "${L:-}" ]; then
START=$((L-15)); END=$((L+25))
echo "--- match: $pat at line $L ---"
sed -n "${START},${END}p" "$FILE_MIG"
fi
done
echo
echo "==> model.ts: map declarations (context)"
for sym in "LEGACY_CLAUDE_FULL_MODEL_MAP" "CLAUDE_CANONICAL_MAP" "LEGACY_CLAUDE_ALIAS_MAP"; do
L=$(rg -n "export const $sym" "$FILE_MODEL" | head -n1 | cut -d: -f1 || true)
if [ -n "${L:-}" ]; then
START=$((L-15)); END=$((L+60))
echo "--- $sym at line $L ---"
sed -n "${START},${END}p" "$FILE_MODEL"
fi
doneRepository: protoLabsAI/protoMaker
Length of output: 15144
Use own-property checks for Claude model map lookups.
legacyId in LEGACY_CLAUDE_FULL_MODEL_MAP (~line 82), legacyId in CLAUDE_CANONICAL_MAP (~line 90), and isLegacyClaudeAlias (id in LEGACY_CLAUDE_ALIAS_MAP, ~line 39) use in on prototype-backed plain objects, so keys like __proto__ can match and cause migrateModelId to return non-string values.
🔧 Suggested fix
- if (legacyId in LEGACY_CLAUDE_FULL_MODEL_MAP) {
+ if (Object.prototype.hasOwnProperty.call(LEGACY_CLAUDE_FULL_MODEL_MAP, legacyId)) {
return LEGACY_CLAUDE_FULL_MODEL_MAP[legacyId];
}
@@
- if (legacyId.startsWith('claude-') && legacyId in CLAUDE_CANONICAL_MAP) {
+ if (
+ legacyId.startsWith('claude-') &&
+ Object.prototype.hasOwnProperty.call(CLAUDE_CANONICAL_MAP, legacyId)
+ ) {
return CLAUDE_CANONICAL_MAP[legacyId as keyof typeof CLAUDE_CANONICAL_MAP];
}
@@
- if (isLegacyClaudeAlias(legacyId)) {
- const canonical = LEGACY_CLAUDE_ALIAS_MAP[legacyId];
+ if (Object.prototype.hasOwnProperty.call(LEGACY_CLAUDE_ALIAS_MAP, legacyId)) {
+ const canonical = LEGACY_CLAUDE_ALIAS_MAP[
+ legacyId as keyof typeof LEGACY_CLAUDE_ALIAS_MAP
+ ];
return CLAUDE_CANONICAL_MAP[canonical];
}🤖 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 `@libs/types/src/model-migration.ts` around lines 79 - 98, The map lookups in
migrateModelId and isLegacyClaudeAlias use the `in` operator (e.g. `legacyId in
LEGACY_CLAUDE_FULL_MODEL_MAP`, `legacyId in CLAUDE_CANONICAL_MAP`, and `id in
LEGACY_CLAUDE_ALIAS_MAP`) which can match prototype keys like "__proto__";
change them to own-property checks using either
`Object.prototype.hasOwnProperty.call(OBJ, key)` or `Object.hasOwn(OBJ, key)` so
only actual map keys are matched, and ensure the code paths in migrateModelId
and isLegacyClaudeAlias then use the returned string values directly (no
prototype values).
Summary
Closes #3661. The gateway-issued API key only allows
protolabs/*tier names, butCLAUDE_CANONICAL_MAPandCLAUDE_MODEL_MAPstill resolved Claude aliases to versioned Anthropic strings (claude-sonnet-4-6, etc.). Every install with pre-cutoverdata/settings.json401'd on the first agent turn.The mapping
claude-haiku/haikuprotolabs/fastclaude-sonnet/sonnetprotolabs/smartclaude-opus/opusprotolabs/reasoningclaude-sonnet-4-6(legacy versioned)protolabs/smartclaude-haiku-4-5-20251001(legacy versioned)protolabs/fastclaude-opus-4-6(legacy versioned)protolabs/reasoningChanges
libs/types/src/model.ts:CLAUDE_CANONICAL_MAP+CLAUDE_MODEL_MAPvalues switched to gateway tiers. NewLEGACY_CLAUDE_FULL_MODEL_MAPcovers persisted versioned strings.libs/types/src/model-migration.ts:migrateModelIdnow rewrites canonical IDs, short aliases, and full versioned strings to gateway tiers in one pass.libs/types/src/global-settings.ts:SETTINGS_VERSION6 → 7.libs/types/src/index.ts: exportLEGACY_CLAUDE_FULL_MODEL_MAP.Test plan
libs/types/tests/model-migration-gateway.test.ts— 22 tests covering all input forms and passthrough behaviorprotolabs/*outputs (greenfield-first — no direct-Anthropic path)What this unblocks
After this lands and the server restarts, the three crew features that 401'd this morning (#3601, #3600, #3605) can redispatch and actually run.
Still outstanding: #3662 (state machine ticking to
reviewdespite agent hard-fail) — separate PR.Closes #3661
Summary by CodeRabbit