Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions apps/server/tests/unit/lib/model-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ describe('model-resolver.ts', () => {
expect(result).toBe(CLAUDE_MODEL_MAP.sonnet);
});

it("should resolve 'opus' alias to full model string", () => {
it("should resolve 'opus' alias to the gateway reasoning tier", () => {
const result = resolveModelString('opus');
expect(result).toBe('claude-opus-4-6');
expect(result).toBe('protolabs/reasoning');
});

it('should pass through unknown models unchanged (may be provider models)', () => {
Expand Down Expand Up @@ -110,7 +110,7 @@ describe('model-resolver.ts', () => {
describe('getEffectiveModel', () => {
it('should prioritize explicit model over session and default', () => {
const result = getEffectiveModel('opus', 'haiku', 'gpt-5.2');
expect(result).toBe('claude-opus-4-6');
expect(result).toBe('protolabs/reasoning');
});

it('should use session model when explicit is not provided', () => {
Expand Down Expand Up @@ -142,10 +142,10 @@ describe('model-resolver.ts', () => {
expect(CLAUDE_MODEL_MAP).toHaveProperty('opus');
});

it('should have valid Claude model strings', () => {
expect(CLAUDE_MODEL_MAP.haiku).toContain('haiku');
expect(CLAUDE_MODEL_MAP.sonnet).toContain('sonnet');
expect(CLAUDE_MODEL_MAP.opus).toContain('opus');
it('maps short aliases to protolabs/* gateway tiers', () => {
expect(CLAUDE_MODEL_MAP.haiku).toBe('protolabs/fast');
expect(CLAUDE_MODEL_MAP.sonnet).toBe('protolabs/smart');
expect(CLAUDE_MODEL_MAP.opus).toBe('protolabs/reasoning');
});
});

Expand Down
6 changes: 4 additions & 2 deletions apps/server/tests/unit/lib/sdk-options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,14 @@ describe('sdk-options.ts', () => {
expect(result).toBe('claude-sonnet-4-20250514');
});

it('should use default model for spec when no override', async () => {
it('should use the gateway default model for spec when no override', async () => {
delete process.env.AUTOMAKER_MODEL_SPEC;
delete process.env.AUTOMAKER_MODEL_DEFAULT;
const { getModelForUseCase } = await import('@/lib/sdk-options.js');
const result = getModelForUseCase('spec');
expect(result).toContain('claude');
// Spec generation now resolves through the protoLabs gateway — direct
// Anthropic strings are not used.
expect(result.startsWith('protolabs/')).toBe(true);
});

it('should fall back to AUTOMAKER_MODEL_DEFAULT', async () => {
Expand Down
12 changes: 6 additions & 6 deletions apps/server/tests/unit/services/execution-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -802,7 +802,7 @@ describe('ExecutionService - getModelForFeature assignedRole', () => {
PROJECT_PATH
);

expect(result.model).toContain('claude-opus-4-5');
expect(result.model).toBe('protolabs/reasoning');
expect(mockGetAgent).toHaveBeenCalledWith(PROJECT_PATH, 'frontend-dev');
});

Expand All @@ -822,7 +822,7 @@ describe('ExecutionService - getModelForFeature assignedRole', () => {
PROJECT_PATH
);

expect(result.model).toContain('claude-haiku-4-5');
expect(result.model).toBe('protolabs/fast');
expect(result.providerId).toBe('anthropic');
});

Expand All @@ -847,7 +847,7 @@ describe('ExecutionService - getModelForFeature assignedRole', () => {
);

// Manifest wins
expect(result.model).toContain('claude-opus-4-5');
expect(result.model).toBe('protolabs/reasoning');
// Settings override should NOT have been consulted for model
expect(mockGetWorkflowSettings).not.toHaveBeenCalled();
});
Expand All @@ -866,7 +866,7 @@ describe('ExecutionService - getModelForFeature assignedRole', () => {
PROJECT_PATH
);

expect(result.model).toContain('claude-sonnet-4-6');
expect(result.model).toBe('protolabs/smart');
expect(mockGetPhaseModelWithOverrides).toHaveBeenCalledWith(
'agentExecutionModel',
null,
Expand All @@ -883,7 +883,7 @@ describe('ExecutionService - getModelForFeature assignedRole', () => {
const svc = makeTestService();
const result = await (svc as any).getModelForFeature({ complexity: 'medium' }, PROJECT_PATH);

expect(result.model).toContain('claude-sonnet-4-6');
expect(result.model).toBe('protolabs/smart');
expect(mockGetAgent).not.toHaveBeenCalled();
expect(mockGetWorkflowSettings).not.toHaveBeenCalled();
});
Expand All @@ -897,7 +897,7 @@ describe('ExecutionService - getModelForFeature assignedRole', () => {
PROJECT_PATH
);

expect(result.model).toContain('claude-haiku-4-5');
expect(result.model).toBe('protolabs/fast');
expect(mockGetAgent).not.toHaveBeenCalled();
});

Expand Down
38 changes: 19 additions & 19 deletions apps/server/tests/unit/services/settings-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,11 +647,12 @@ describe('settings-service.ts', () => {

const settings = await settingsService.getGlobalSettings();

// Verify all phase models are now PhaseModelEntry objects
// Legacy aliases are migrated to canonical IDs
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'claude-sonnet' });
expect(settings.phaseModels.fileDescriptionModel).toEqual({ model: 'claude-haiku' });
expect(settings.phaseModels.specGenerationModel).toEqual({ model: 'claude-opus' });
// Verify all phase models are now PhaseModelEntry objects. Legacy short
// aliases (sonnet/haiku/opus) migrate straight to the gateway tiers —
// there is no direct-Anthropic path anymore.
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'protolabs/smart' });
expect(settings.phaseModels.fileDescriptionModel).toEqual({ model: 'protolabs/fast' });
expect(settings.phaseModels.specGenerationModel).toEqual({ model: 'protolabs/reasoning' });
expect(settings.version).toBe(SETTINGS_VERSION);
});

Expand All @@ -676,18 +677,17 @@ describe('settings-service.ts', () => {

const settings = await settingsService.getGlobalSettings();

// Verify PhaseModelEntry objects are preserved with thinkingLevel
// Legacy aliases are migrated to canonical IDs
// Short aliases migrate to gateway tiers; thinkingLevel is preserved.
expect(settings.phaseModels.enhancementModel).toEqual({
model: 'claude-sonnet',
model: 'protolabs/smart',
thinkingLevel: 'high',
});
expect(settings.phaseModels.specGenerationModel).toEqual({
model: 'claude-opus',
model: 'protolabs/reasoning',
thinkingLevel: 'ultrathink',
});
expect(settings.phaseModels.backlogPlanningModel).toEqual({
model: 'claude-sonnet',
model: 'protolabs/smart',
thinkingLevel: 'medium',
});
});
Expand All @@ -714,14 +714,14 @@ describe('settings-service.ts', () => {
const settings = await settingsService.getGlobalSettings();

// Strings should be converted to objects with canonical IDs
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'claude-sonnet' });
expect(settings.phaseModels.imageDescriptionModel).toEqual({ model: 'claude-haiku' });
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'protolabs/smart' });
expect(settings.phaseModels.imageDescriptionModel).toEqual({ model: 'protolabs/fast' });
// Objects should be preserved with migrated IDs
expect(settings.phaseModels.fileDescriptionModel).toEqual({
model: 'claude-haiku',
model: 'protolabs/fast',
thinkingLevel: 'low',
});
expect(settings.phaseModels.validationModel).toEqual({ model: 'claude-opus' });
expect(settings.phaseModels.validationModel).toEqual({ model: 'protolabs/reasoning' });
});

it('should migrate legacy enhancementModel/validationModel fields', async () => {
Expand All @@ -739,8 +739,8 @@ describe('settings-service.ts', () => {
const settings = await settingsService.getGlobalSettings();

// Legacy fields should be migrated to phaseModels with canonical IDs
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'claude-haiku' });
expect(settings.phaseModels.validationModel).toEqual({ model: 'claude-opus' });
expect(settings.phaseModels.enhancementModel).toEqual({ model: 'protolabs/fast' });
expect(settings.phaseModels.validationModel).toEqual({ model: 'protolabs/reasoning' });
// Other fields should use defaults (DEFAULT_PHASE_MODELS, gateway-routed)
expect(settings.phaseModels.specGenerationModel).toEqual({ model: 'protolabs/reasoning' });
});
Expand Down Expand Up @@ -779,13 +779,13 @@ describe('settings-service.ts', () => {

const settings = await settingsService.getGlobalSettings();

// Both should be preserved (models migrated to canonical format)
// Both should be preserved (models migrated to gateway tiers)
expect(settings.phaseModels.enhancementModel).toEqual({
model: 'claude-sonnet',
model: 'protolabs/smart',
thinkingLevel: 'high',
});
expect(settings.phaseModels.specGenerationModel).toEqual({
model: 'claude-opus',
model: 'protolabs/reasoning',
thinkingLevel: 'ultrathink',
});
});
Expand Down
46 changes: 23 additions & 23 deletions libs/model-resolver/tests/resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,10 @@ describe('model-resolver', () => {
expect(result).toBe(fullModel);
});

it('should handle claude-opus model strings', () => {
const fullModel = 'claude-opus-4-6';
const result = resolveModelString(fullModel);
it('rewrites legacy versioned claude-opus strings to the gateway reasoning tier', () => {
const result = resolveModelString('claude-opus-4-6');

expect(result).toBe(fullModel);
expect(result).toBe('protolabs/reasoning');
});

it('should handle claude-haiku model strings', () => {
Expand Down Expand Up @@ -76,11 +75,11 @@ describe('model-resolver', () => {
expect(result).toBe(CLAUDE_MODEL_MAP.haiku);
});

it('should resolve all known aliases to valid Claude model strings', () => {
it('resolves all known short aliases to a protolabs/* gateway tier', () => {
const aliases = ['sonnet', 'opus', 'haiku'];
for (const alias of aliases) {
const resolved = resolveModelString(alias);
expect(resolved).toContain('claude-');
expect(resolved.startsWith('protolabs/')).toBe(true);
expect(resolved).toBe(CLAUDE_MODEL_MAP[alias]);
}
});
Expand Down Expand Up @@ -200,26 +199,28 @@ describe('model-resolver', () => {
describe('getEffectiveModel', () => {
describe('priority handling', () => {
it('should prioritize explicit model over all others', () => {
const explicit = 'claude-opus-4-6';
const session = 'claude-sonnet-4-6';
const defaultModel = 'claude-haiku-4-5-20251001';
// Use gateway-native tiers; legacy claude-X-Y-Z strings get migrated
// by the resolver and would no longer compare equal to themselves.
const explicit = 'protolabs/reasoning';
const session = 'protolabs/smart';
const defaultModel = 'protolabs/fast';

const result = getEffectiveModel(explicit, session, defaultModel);

expect(result).toBe(explicit);
});

it('should use session model when explicit is undefined', () => {
const session = 'claude-sonnet-4-6';
const defaultModel = 'claude-haiku-4-5-20251001';
const session = 'protolabs/smart';
const defaultModel = 'protolabs/fast';

const result = getEffectiveModel(undefined, session, defaultModel);

expect(result).toBe(session);
});

it('should use default model when both explicit and session are undefined', () => {
const defaultModel = 'claude-opus-4-6';
const defaultModel = 'protolabs/reasoning';

const result = getEffectiveModel(undefined, undefined, defaultModel);

Expand Down Expand Up @@ -247,23 +248,23 @@ describe('model-resolver', () => {
});

it('should prioritize explicit alias over session full string', () => {
const result = getEffectiveModel('sonnet', 'claude-opus-4-6');
const result = getEffectiveModel('sonnet', 'protolabs/reasoning');

expect(result).toBe(CLAUDE_MODEL_MAP.sonnet);
});
});

describe('with empty strings', () => {
it('should treat empty explicit string as undefined', () => {
const session = 'claude-sonnet-4-6';
const session = 'protolabs/smart';

const result = getEffectiveModel('', session);

expect(result).toBe(session);
});

it('should treat empty session string as undefined', () => {
const defaultModel = 'claude-opus-4-6';
const defaultModel = 'protolabs/reasoning';

const result = getEffectiveModel(undefined, '', defaultModel);

Expand Down Expand Up @@ -306,13 +307,13 @@ describe('model-resolver', () => {
});

describe('CLAUDE_MODEL_MAP integration', () => {
it('should have valid mappings for all known aliases', () => {
it('maps all known aliases to a protolabs/* gateway tier', () => {
const aliases = ['sonnet', 'opus', 'haiku'];

for (const alias of aliases) {
const resolved = resolveModelString(alias);
expect(resolved).toBeDefined();
expect(resolved).toContain('claude-');
expect(resolved.startsWith('protolabs/')).toBe(true);
expect(resolved).toBe(CLAUDE_MODEL_MAP[alias]);
}
});
Expand Down Expand Up @@ -379,11 +380,10 @@ describe('model-resolver', () => {
expect(result.thinkingLevel).toBeUndefined();
});

it('should pass through full Claude model string', () => {
const fullModel = 'claude-sonnet-4-6';
const result = resolvePhaseModel(fullModel);
it('migrates legacy full claude-X-Y-Z strings to the gateway tier', () => {
const result = resolvePhaseModel('claude-sonnet-4-6');

expect(result.model).toBe(fullModel);
expect(result.model).toBe('protolabs/smart');
expect(result.thinkingLevel).toBeUndefined();
});

Expand Down Expand Up @@ -444,14 +444,14 @@ describe('model-resolver', () => {
expect(result.thinkingLevel).toBe('ultrathink');
});

it('should handle full Claude model string in entry', () => {
it('migrates legacy full claude-X-Y-Z entries to gateway tier and preserves thinkingLevel', () => {
const entry: PhaseModelEntry = {
model: 'claude-opus-4-6',
thinkingLevel: 'high',
};
const result = resolvePhaseModel(entry);

expect(result.model).toBe('claude-opus-4-6');
expect(result.model).toBe('protolabs/reasoning');
expect(result.thinkingLevel).toBe('high');
});
});
Expand Down
2 changes: 1 addition & 1 deletion libs/types/src/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export type { ModelAlias };
// ============================================================================

/** Current version of the global settings schema */
export const SETTINGS_VERSION = 6;
export const SETTINGS_VERSION = 7;
/** Current version of the credentials schema */
export const CREDENTIALS_VERSION = 1;

Expand Down
1 change: 1 addition & 0 deletions libs/types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export {
CLAUDE_MODEL_MAP,
CLAUDE_CANONICAL_MAP,
LEGACY_CLAUDE_ALIAS_MAP,
LEGACY_CLAUDE_FULL_MODEL_MAP,
CODEX_MODEL_MAP,
CODEX_MODEL_IDS,
REASONING_CAPABLE_MODELS,
Expand Down
26 changes: 21 additions & 5 deletions libs/types/src/model-migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,12 @@ import { LEGACY_CURSOR_MODEL_MAP, CURSOR_MODEL_MAP } from './cursor-models.js';
import type { OpencodeModelId, LegacyOpencodeModelId } from './opencode-models.js';
import { LEGACY_OPENCODE_MODEL_MAP, OPENCODE_MODEL_CONFIG_MAP } from './opencode-models.js';
import type { ClaudeCanonicalId } from './model.js';
import { LEGACY_CLAUDE_ALIAS_MAP, CLAUDE_CANONICAL_MAP, CLAUDE_MODEL_MAP } from './model.js';
import {
LEGACY_CLAUDE_ALIAS_MAP,
LEGACY_CLAUDE_FULL_MODEL_MAP,
CLAUDE_CANONICAL_MAP,
CLAUDE_MODEL_MAP,
} from './model.js';
import type { PhaseModelEntry } from './settings.js';

/**
Expand Down Expand Up @@ -71,14 +76,25 @@ export function migrateModelId(legacyId: string | undefined | null): string {
return LEGACY_OPENCODE_MODEL_MAP[legacyId];
}

// Already has claude- prefix and is in canonical map
// 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];
}
Comment on lines +79 to 98
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 | ⚡ Quick win

🧩 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
fi

Repository: 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
done

Repository: 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).


// Unknown or already canonical - pass through
Expand Down
Loading
Loading