mirror of
https://github.com/Gitlawb/openclaude.git
synced 2026-08-24 10:14:19 -05:00
fix(model): resolve [1m]-tagged aliases when 1M context is disabled (#1822)
* fix(model): resolve [1m]-tagged aliases when 1M context is disabled parseUserSpecifiedModel gated stripping of the [1m] tag on has1mContext, which returns false when CLAUDE_CODE_DISABLE_1M_CONTEXT is set (the C4E/HIPAA admin control). With the flag on, an aliased request like 'sonnet[1m]' kept the tag attached, never matched the 'sonnet' alias, and returned the literal unservable string 'sonnet[1m]' — so disabling 1M broke [1m]-aliased model selection entirely instead of gracefully serving the base model. Separate 'tag is present in the input' (always strip before matching) from '1M is active' (re-append the suffix). When 1M is disabled, the tag is dropped and the alias/custom id resolves to its base model. Covers the alias and custom-model paths; the ant path already stripped unconditionally. Adds regression coverage for both the disabled (tag dropped, base resolved) and enabled (tag preserved) directions. * fix(model): drop disabled [1m] from custom default alias targets The alias branches resolved to getDefaultSonnetModel()/getDefaultOpusModel() etc. and appended the tag, but a custom default override such as ANTHROPIC_DEFAULT_SONNET_MODEL=MySonnetDeploy[1m] bakes the suffix into the resolved value. With CLAUDE_CODE_DISABLE_1M_CONTEXT=1 a request like `sonnet[1m]` still resolved to `MySonnetDeploy[1m]`, leaving the disabled tag on (and a tagged input doubled it to `...[1m][1m]`). Route the alias- and legacy-opus-resolved defaults through a small helper that strips whatever [1m] is present and re-attaches it only when a tag was requested (input or resolved default) AND 1M context is enabled. Disabled 1M now drops the tag regardless of source; enabled still honors an env default's opt-in without duplicating it. Custom deployment casing is preserved. Adds coverage for default-model env overrides (disabled + enabled directions) and a mixed-case custom id. * test(model): cover disabled-1M mixed-case custom ids and Codex aliases The disabled-1M block covered the Claude-family aliases and a lowercase custom id, but not two paths flagged in review: a mixed-case custom deployment id (must drop [1m]/[1M] while preserving casing) and the Codex aliases, which are resolved by a separate branch from the Claude-family aliases and so need their own disabled-1M assertions (codexplan[1m] / codexspark[1M] drop the tag).
This commit is contained in:
+35
-11
@@ -756,23 +756,42 @@ export function parseUserSpecifiedModel(
|
||||
}
|
||||
const normalizedModel = modelInputTrimmed.toLowerCase()
|
||||
|
||||
// Separate "the [1m] tag is present in the input" from "1M context is active".
|
||||
// The tag must ALWAYS be stripped before alias/model matching, otherwise an
|
||||
// aliased request like `sonnet[1m]` fails to resolve to its base model. Whether
|
||||
// to re-append the tag depends on has1mContext, which returns false when 1M is
|
||||
// disabled (CLAUDE_CODE_DISABLE_1M_CONTEXT) — in that case the request resolves
|
||||
// to the base model with the tag dropped, not left as an unresolved alias.
|
||||
const hasTagSyntax = /\[1m]$/i.test(normalizedModel)
|
||||
const has1mTag = has1mContext(normalizedModel)
|
||||
const modelString = has1mTag
|
||||
const modelString = hasTagSyntax
|
||||
? normalizedModel.replace(/\[1m]$/i, '').trim()
|
||||
: normalizedModel
|
||||
|
||||
// Re-apply the [1m] tag policy to a resolved model. The resolved value may
|
||||
// itself carry a [1m] suffix — e.g. a custom default override like
|
||||
// ANTHROPIC_DEFAULT_SONNET_MODEL=Deploy[1m] baked into getDefaultSonnetModel().
|
||||
// Strip whatever tag is present, then re-attach [1m] only when a tag was
|
||||
// requested (on the user input OR the resolved default) AND 1M context is
|
||||
// enabled. This guarantees CLAUDE_CODE_DISABLE_1M_CONTEXT drops the tag no
|
||||
// matter where it came from, while still honoring an env default's opt-in.
|
||||
const applyOneMTag = (resolved: ModelName): ModelName => {
|
||||
const base = resolved.replace(/\[1m]$/i, '').trim()
|
||||
return has1mTag || has1mContext(resolved) ? base + '[1m]' : base
|
||||
}
|
||||
|
||||
if (isModelAlias(modelString)) {
|
||||
switch (modelString) {
|
||||
case 'opusplan':
|
||||
return getDefaultSonnetModel() + (has1mTag ? '[1m]' : '') // Sonnet is default, Opus in plan mode
|
||||
return applyOneMTag(getDefaultSonnetModel()) // Sonnet is default, Opus in plan mode
|
||||
case 'sonnet':
|
||||
return getDefaultSonnetModel() + (has1mTag ? '[1m]' : '')
|
||||
return applyOneMTag(getDefaultSonnetModel())
|
||||
case 'haiku':
|
||||
return getDefaultHaikuModel() + (has1mTag ? '[1m]' : '')
|
||||
return applyOneMTag(getDefaultHaikuModel())
|
||||
case 'opus':
|
||||
return getDefaultOpusModel() + (has1mTag ? '[1m]' : '')
|
||||
return applyOneMTag(getDefaultOpusModel())
|
||||
case 'best':
|
||||
return getBestModel() + (has1mTag ? '[1m]' : '')
|
||||
return applyOneMTag(getBestModel())
|
||||
default:
|
||||
}
|
||||
}
|
||||
@@ -799,7 +818,7 @@ export function parseUserSpecifiedModel(
|
||||
isLegacyOpusFirstParty(modelString) &&
|
||||
isLegacyModelRemapEnabled()
|
||||
) {
|
||||
return getDefaultOpusModel() + (has1mTag ? '[1m]' : '')
|
||||
return applyOneMTag(getDefaultOpusModel())
|
||||
}
|
||||
|
||||
if (process.env.USER_TYPE === 'ant') {
|
||||
@@ -817,10 +836,15 @@ export function parseUserSpecifiedModel(
|
||||
// can tell the user to restart/wait for flag cache refresh to get the latest values.
|
||||
}
|
||||
|
||||
// Preserve original case for custom model names (e.g., Azure Foundry deployment IDs)
|
||||
// Only strip [1m] suffix if present, maintaining case of the base model
|
||||
if (has1mTag) {
|
||||
return modelInputTrimmed.replace(/\[1m\]$/i, '').trim() + '[1m]'
|
||||
// Preserve original case for custom model names (e.g., Azure Foundry deployment IDs).
|
||||
// Strip a present [1m] suffix (maintaining base-model case) and re-append it
|
||||
// only when 1M is active — when disabled, a custom `mydeploy[1m]` must resolve
|
||||
// to the base `mydeploy`, not an unservable `mydeploy[1m]` model id.
|
||||
if (hasTagSyntax) {
|
||||
return (
|
||||
modelInputTrimmed.replace(/\[1m\]$/i, '').trim() +
|
||||
(has1mTag ? '[1m]' : '')
|
||||
)
|
||||
}
|
||||
return modelInputTrimmed
|
||||
}
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
import { afterEach, beforeEach, describe, expect, test } from 'bun:test'
|
||||
import { parseUserSpecifiedModel } from './model.js'
|
||||
|
||||
// Regression: when 1M context is disabled (CLAUDE_CODE_DISABLE_1M_CONTEXT, used
|
||||
// by C4E/HIPAA admins), `has1mContext` returns false. The parser gated the
|
||||
// stripping of the `[1m]` tag on that flag, so an aliased request like
|
||||
// `sonnet[1m]` kept the tag attached, never matched the `sonnet` alias, and
|
||||
// returned the literal, unservable string `sonnet[1m]`. The tag must be stripped
|
||||
// for matching regardless; only the re-appended suffix depends on 1M being on.
|
||||
describe('parseUserSpecifiedModel — [1m] tag when 1M context is disabled', () => {
|
||||
const original = process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT
|
||||
|
||||
beforeEach(() => {
|
||||
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1'
|
||||
})
|
||||
afterEach(() => {
|
||||
if (original === undefined) {
|
||||
delete process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = original
|
||||
}
|
||||
})
|
||||
|
||||
for (const alias of ['sonnet', 'opus', 'haiku', 'best', 'opusplan']) {
|
||||
test(`${alias}[1m] resolves to the base model (no [1m]) when 1M is disabled`, () => {
|
||||
const base = parseUserSpecifiedModel(alias)
|
||||
const tagged = parseUserSpecifiedModel(`${alias}[1m]`)
|
||||
// Base model is returned, with the 1M tag dropped — not a literal alias.
|
||||
expect(tagged).toBe(base)
|
||||
expect(tagged.endsWith('[1m]')).toBe(false)
|
||||
expect(tagged).not.toBe(`${alias}[1m]`)
|
||||
})
|
||||
}
|
||||
|
||||
test('case-insensitive tag is also resolved (SONNET[1M] → base sonnet)', () => {
|
||||
expect(parseUserSpecifiedModel('SONNET[1M]')).toBe(
|
||||
parseUserSpecifiedModel('sonnet'),
|
||||
)
|
||||
})
|
||||
|
||||
test('custom model id drops the [1m] suffix when 1M is disabled', () => {
|
||||
expect(parseUserSpecifiedModel('my-custom-deploy[1m]')).toBe(
|
||||
'my-custom-deploy',
|
||||
)
|
||||
})
|
||||
|
||||
test('mixed-case custom id drops [1m]/[1M] but preserves casing when disabled', () => {
|
||||
expect(parseUserSpecifiedModel('MyCustomDeploy[1M]')).toBe('MyCustomDeploy')
|
||||
expect(parseUserSpecifiedModel('MyCustomDeploy[1m]')).toBe('MyCustomDeploy')
|
||||
})
|
||||
|
||||
// Codex aliases are resolved by a separate branch from the Claude-family
|
||||
// aliases, so they need their own disabled-1M coverage.
|
||||
test('codex aliases drop the [1m] tag when 1M is disabled', () => {
|
||||
const codexplan = parseUserSpecifiedModel('codexplan')
|
||||
const codexspark = parseUserSpecifiedModel('codexspark')
|
||||
expect(parseUserSpecifiedModel('codexplan[1m]')).toBe(codexplan)
|
||||
expect(parseUserSpecifiedModel('codexspark[1M]')).toBe(codexspark)
|
||||
expect(parseUserSpecifiedModel('codexplan[1m]').endsWith('[1m]')).toBe(false)
|
||||
expect(parseUserSpecifiedModel('codexspark[1M]').endsWith('[1m]')).toBe(
|
||||
false,
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Guard the opposite direction: with 1M enabled (default), the tag is preserved.
|
||||
describe('parseUserSpecifiedModel — [1m] tag when 1M context is enabled', () => {
|
||||
const original = process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT
|
||||
|
||||
beforeEach(() => {
|
||||
delete process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT
|
||||
})
|
||||
afterEach(() => {
|
||||
if (original === undefined) {
|
||||
delete process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = original
|
||||
}
|
||||
})
|
||||
|
||||
test('sonnet[1m] keeps the tag on the resolved base model', () => {
|
||||
const base = parseUserSpecifiedModel('sonnet')
|
||||
expect(parseUserSpecifiedModel('sonnet[1m]')).toBe(`${base}[1m]`)
|
||||
})
|
||||
|
||||
test('custom model id keeps the [1m] suffix', () => {
|
||||
expect(parseUserSpecifiedModel('my-custom-deploy[1m]')).toBe(
|
||||
'my-custom-deploy[1m]',
|
||||
)
|
||||
})
|
||||
|
||||
// Mixed-case custom deployment ids keep their casing; only the [1m]/[1M] tag
|
||||
// is normalized/reattached.
|
||||
test('mixed-case custom id preserves casing, tag normalized to [1m]', () => {
|
||||
expect(parseUserSpecifiedModel('MyCustomDeploy[1M]')).toBe(
|
||||
'MyCustomDeploy[1m]',
|
||||
)
|
||||
expect(parseUserSpecifiedModel('MyCustomDeploy[1m]')).toBe(
|
||||
'MyCustomDeploy[1m]',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
// Custom default-model env overrides (ANTHROPIC_DEFAULT_SONNET_MODEL etc.) can
|
||||
// bake a [1m] suffix into the resolved alias target. The disabled-1M path must
|
||||
// strip that too, and the enabled path must honor it without duplicating it.
|
||||
describe('parseUserSpecifiedModel — [1m] on custom default env overrides', () => {
|
||||
const KEYS = [
|
||||
'CLAUDE_CODE_DISABLE_1M_CONTEXT',
|
||||
'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||
] as const
|
||||
const saved: Record<string, string | undefined> = {}
|
||||
|
||||
beforeEach(() => {
|
||||
for (const k of KEYS) saved[k] = process.env[k]
|
||||
process.env.ANTHROPIC_DEFAULT_SONNET_MODEL = 'MySonnetDeploy[1m]'
|
||||
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = 'MyOpusDeploy[1M]'
|
||||
})
|
||||
afterEach(() => {
|
||||
for (const k of KEYS) {
|
||||
if (saved[k] === undefined) delete process.env[k]
|
||||
else process.env[k] = saved[k]
|
||||
}
|
||||
})
|
||||
|
||||
test('disabled: alias drops the baked [1m] from the resolved default', () => {
|
||||
process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT = '1'
|
||||
// Casing of the custom deployment id is preserved; only the tag is dropped.
|
||||
expect(parseUserSpecifiedModel('sonnet[1m]')).toBe('MySonnetDeploy')
|
||||
expect(parseUserSpecifiedModel('sonnet')).toBe('MySonnetDeploy')
|
||||
expect(parseUserSpecifiedModel('opus[1M]')).toBe('MyOpusDeploy')
|
||||
})
|
||||
|
||||
test('enabled: baked [1m] honored and never duplicated', () => {
|
||||
delete process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT
|
||||
// Bare alias honors the env default's opt-in; tag normalized to [1m].
|
||||
expect(parseUserSpecifiedModel('sonnet')).toBe('MySonnetDeploy[1m]')
|
||||
// Tagged alias does not double the suffix.
|
||||
expect(parseUserSpecifiedModel('sonnet[1m]')).toBe('MySonnetDeploy[1m]')
|
||||
expect(parseUserSpecifiedModel('opus')).toBe('MyOpusDeploy[1m]')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user