mirror of
https://github.com/NanmiCoder/claude-code-haha.git
synced 2026-10-10 03:43:11 +08:00
fix(agent-teams): inherit the lead model instead of hardcoded Opus
Unconfigured teammates were spawned as claude-opus-4-8, so mapped third-party providers such as cc-switch DeepSeek billed the expensive fallback. Agent page aliases now resolve through the provider mapping.
This commit is contained in:
@@ -44,7 +44,6 @@ import type { LocalJSXCommandContext, CommandResultDisplay } from '../../command
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from '../../services/analytics/growthbook.js';
|
||||
import { isAgentSwarmsEnabled } from '../../utils/agentSwarmsEnabled.js';
|
||||
import { getCliTeammateModeOverride, clearCliTeammateModeOverride } from '../../utils/swarm/backends/teammateModeSnapshot.js';
|
||||
import { getHardcodedTeammateModelFallback } from '../../utils/swarm/teammateModel.js';
|
||||
import { useSearchInput } from '../../hooks/useSearchInput.js';
|
||||
import { useTerminalSize } from '../../hooks/useTerminalSize.js';
|
||||
import { clearFastModeCooldown, FAST_MODE_MODEL_DISPLAY, isFastModeAvailable, isFastModeEnabled, getFastModeModel, isFastModeSupportedByModel } from '../../utils/fastMode.js';
|
||||
@@ -1540,8 +1539,8 @@ export function Config({
|
||||
setShowSubmenu(null);
|
||||
setTabsHidden(false);
|
||||
// First-open-then-Enter from unset: picker highlights "Default"
|
||||
// (initial=null) and confirming would write null, silently
|
||||
// switching Opus-fallback → follow-leader. Treat as no-op.
|
||||
// (initial=null) and confirming would write null. Unset already
|
||||
// follows the leader, so treat as no-op.
|
||||
if (globalConfig.teammateDefaultModel === undefined && model_1 === null) {
|
||||
return;
|
||||
}
|
||||
@@ -1789,10 +1788,7 @@ export function Config({
|
||||
</Box>;
|
||||
}
|
||||
function teammateModelDisplayString(value: string | null | undefined): string {
|
||||
if (value === undefined) {
|
||||
return modelDisplayString(getHardcodedTeammateModelFallback());
|
||||
}
|
||||
if (value === null) return "Default (leader's model)";
|
||||
if (value === undefined || value === null) return "Default (leader's model)";
|
||||
return modelDisplayString(value);
|
||||
}
|
||||
const THEME_LABELS: Record<string, string> = {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
import type { AppState } from '../../state/AppState.js'
|
||||
import type { ToolUseContext } from '../../Tool.js'
|
||||
import { parseUserSpecifiedModel } from '../../utils/model/model.js'
|
||||
import type {
|
||||
CustomAgentDefinition,
|
||||
PluginAgentDefinition,
|
||||
@@ -213,7 +214,7 @@ describe('Agent Teams custom Agent runtime call sites', () => {
|
||||
makeContext(pluginAgent),
|
||||
)
|
||||
|
||||
expect(result.data.model).toBe('opus')
|
||||
expect(result.data.model).toBe(parseUserSpecifiedModel('opus'))
|
||||
const runnerConfig = startInProcessTeammateMock.mock.calls[0]?.[0] as {
|
||||
agentDefinition?: PluginAgentDefinition
|
||||
}
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test'
|
||||
import { getGlobalConfig, saveGlobalConfig } from '../../utils/config.js'
|
||||
import { parseUserSpecifiedModel } from '../../utils/model/model.js'
|
||||
import {
|
||||
buildTeammateRuntimeCliFlags,
|
||||
resolveTeammateModel,
|
||||
} from './spawnMultiAgent.js'
|
||||
|
||||
const originalSubagentModel = process.env.CLAUDE_CODE_SUBAGENT_MODEL
|
||||
const originalOpusModel = process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
||||
const originalTeammateDefaultModel = getGlobalConfig().teammateDefaultModel
|
||||
|
||||
afterEach(() => {
|
||||
@@ -14,6 +16,11 @@ afterEach(() => {
|
||||
} else {
|
||||
process.env.CLAUDE_CODE_SUBAGENT_MODEL = originalSubagentModel
|
||||
}
|
||||
if (originalOpusModel === undefined) {
|
||||
delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
||||
} else {
|
||||
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = originalOpusModel
|
||||
}
|
||||
saveGlobalConfig(current => ({
|
||||
...current,
|
||||
teammateDefaultModel: originalTeammateDefaultModel,
|
||||
@@ -75,7 +82,7 @@ describe('buildTeammateRuntimeCliFlags', () => {
|
||||
|
||||
expect(
|
||||
resolveTeammateModel('haiku', 'claude-sonnet-4-6', 'gpt-5.6-luna'),
|
||||
).toBe('haiku')
|
||||
).toBe(parseUserSpecifiedModel('haiku'))
|
||||
})
|
||||
|
||||
test('treats a global inherit override as normal teammate resolution', () => {
|
||||
@@ -84,7 +91,9 @@ describe('buildTeammateRuntimeCliFlags', () => {
|
||||
...current,
|
||||
teammateDefaultModel: null,
|
||||
}))
|
||||
expect(resolveTeammateModel('haiku', 'claude-sonnet-4-6')).toBe('haiku')
|
||||
expect(resolveTeammateModel('haiku', 'claude-sonnet-4-6')).toBe(
|
||||
parseUserSpecifiedModel('haiku'),
|
||||
)
|
||||
expect(resolveTeammateModel(undefined, 'claude-sonnet-4-6')).toBe(
|
||||
'claude-sonnet-4-6',
|
||||
)
|
||||
@@ -113,6 +122,41 @@ describe('buildTeammateRuntimeCliFlags', () => {
|
||||
).toBe('gpt-5.6-luna')
|
||||
})
|
||||
|
||||
test('unset teammateDefaultModel follows the leader instead of a hardcoded Opus ID', () => {
|
||||
delete process.env.CLAUDE_CODE_SUBAGENT_MODEL
|
||||
saveGlobalConfig(current => {
|
||||
const next = { ...current }
|
||||
delete next.teammateDefaultModel
|
||||
return next
|
||||
})
|
||||
|
||||
expect(
|
||||
resolveTeammateModel(
|
||||
undefined,
|
||||
'deepseek-flash',
|
||||
undefined,
|
||||
false,
|
||||
),
|
||||
).toBe('deepseek-flash')
|
||||
})
|
||||
|
||||
test('an Agent page opus pin resolves through the provider mapping', () => {
|
||||
delete process.env.CLAUDE_CODE_SUBAGENT_MODEL
|
||||
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = 'deepseek-flash'
|
||||
saveGlobalConfig(current => {
|
||||
const next = { ...current }
|
||||
delete next.teammateDefaultModel
|
||||
return next
|
||||
})
|
||||
|
||||
expect(
|
||||
resolveTeammateModel(undefined, 'deepseek-flash', 'opus', true),
|
||||
).toBe('deepseek-flash')
|
||||
expect(
|
||||
resolveTeammateModel(undefined, 'deepseek-flash', 'inherit', true),
|
||||
).toBe('deepseek-flash')
|
||||
})
|
||||
|
||||
test('selected Agents inherit the leader when model is omitted or inherit', () => {
|
||||
delete process.env.CLAUDE_CODE_SUBAGENT_MODEL
|
||||
saveGlobalConfig(current => ({
|
||||
|
||||
@@ -79,14 +79,13 @@ import {
|
||||
|
||||
function getDefaultTeammateModel(leaderModel: string | null): string {
|
||||
const configured = getGlobalConfig().teammateDefaultModel
|
||||
if (configured === null) {
|
||||
// User picked "Default" in the /config picker — follow the leader.
|
||||
return leaderModel ?? getHardcodedTeammateModelFallback()
|
||||
}
|
||||
if (configured !== undefined) {
|
||||
if (typeof configured === 'string' && configured.trim()) {
|
||||
return parseUserSpecifiedModel(configured)
|
||||
}
|
||||
return getHardcodedTeammateModelFallback()
|
||||
// Unset (`undefined`) and /config "Default" (`null`) both follow the leader.
|
||||
// A first-party Opus ID here is what sent mapped third-party teammates
|
||||
// (cc-switch DeepSeek, etc.) to the upstream's most expensive model.
|
||||
return leaderModel ?? getHardcodedTeammateModelFallback()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -126,7 +125,7 @@ export function resolveTeammateModel(
|
||||
if (invocationModel.toLowerCase() === 'inherit') {
|
||||
return leaderModel ?? getDefaultTeammateModel(leaderModel)
|
||||
}
|
||||
return invocationModel
|
||||
return parseUserSpecifiedModel(invocationModel)
|
||||
}
|
||||
|
||||
if (hasAgentDefinition) {
|
||||
@@ -135,15 +134,18 @@ export function resolveTeammateModel(
|
||||
definitionModel &&
|
||||
definitionModel.toLowerCase() !== 'inherit'
|
||||
) {
|
||||
return definitionModel
|
||||
// Agent page / frontmatter is the highest teammate-specific pin.
|
||||
// Resolve aliases so `opus` on a mapped provider becomes that
|
||||
// provider's opus slot, not a first-party Opus ID.
|
||||
return parseUserSpecifiedModel(definitionModel)
|
||||
}
|
||||
// Official Agent frontmatter defaults to `inherit`, so a selected Agent
|
||||
// follows the leader when its model is omitted or explicitly inherit.
|
||||
return leaderModel ?? getDefaultTeammateModel(leaderModel)
|
||||
}
|
||||
|
||||
// Plain teammates retain the existing /config teammateDefaultModel
|
||||
// contract. Do not let the presence of a leader bypass that setting.
|
||||
// Plain teammates: a pinned /config teammateDefaultModel still wins;
|
||||
// otherwise follow the leader (same as an Agent whose model is inherit).
|
||||
return getDefaultTeammateModel(leaderModel)
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -536,7 +536,7 @@ export type GlobalConfig = {
|
||||
// Teammate spawn mode: 'auto' | 'tmux' | 'in-process'
|
||||
teammateMode?: 'auto' | 'tmux' | 'in-process' // How to spawn teammates (default: 'auto')
|
||||
// Model for new teammates when the tool call doesn't pass one.
|
||||
// undefined = hardcoded Opus (backward-compat); null = leader's model; string = model alias/ID.
|
||||
// undefined/null = leader's model; string = model alias/ID.
|
||||
teammateDefaultModel?: string | null
|
||||
|
||||
// PR status footer configuration (feature-flagged via GrowthBook)
|
||||
|
||||
@@ -31,6 +31,7 @@ const ENV_KEYS = [
|
||||
'ANTHROPIC_MODEL',
|
||||
'ANTHROPIC_DEFAULT_FABLE_MODEL',
|
||||
'ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||
'CLAUDE_CODE_DISABLE_1M_CONTEXT',
|
||||
'CLAUDE_CODE_USE_BEDROCK',
|
||||
'CLAUDE_CODE_SUBAGENT_MODEL',
|
||||
@@ -182,6 +183,10 @@ describe('Fable model configuration', () => {
|
||||
expect(sanitizeModelName('claude-sonnet-5-experimental')).toBe('claude-sonnet-5')
|
||||
expect(getHardcodedTeammateModelFallback()).toBe('claude-opus-4-8')
|
||||
|
||||
process.env.ANTHROPIC_DEFAULT_OPUS_MODEL = 'deepseek-flash'
|
||||
expect(getHardcodedTeammateModelFallback()).toBe('deepseek-flash')
|
||||
delete process.env.ANTHROPIC_DEFAULT_OPUS_MODEL
|
||||
|
||||
process.env.CLAUDE_CODE_USE_BEDROCK = '1'
|
||||
expect(getHardcodedTeammateModelFallback()).toBe(
|
||||
CLAUDE_OPUS_4_6_CONFIG.bedrock,
|
||||
|
||||
@@ -3,9 +3,14 @@ import { getAPIProvider } from '../model/providers.js'
|
||||
|
||||
// @[MODEL LAUNCH]: Update the fallback model below.
|
||||
// When the user has never set teammateDefaultModel in /config, new teammates
|
||||
// use the current Opus default. Must be provider-aware so Bedrock/Vertex/Foundry
|
||||
// customers get a conservative provider ID.
|
||||
// use the current Opus default. Honor ANTHROPIC_DEFAULT_OPUS_MODEL first so a
|
||||
// mapped third-party provider (cc-switch DeepSeek, etc.) is not rewritten to a
|
||||
// first-party Opus ID that the upstream then bills as its most expensive model.
|
||||
// Bedrock/Vertex/Foundry still get a conservative provider ID.
|
||||
export function getHardcodedTeammateModelFallback(): string {
|
||||
const mappedOpus = process.env.ANTHROPIC_DEFAULT_OPUS_MODEL?.trim()
|
||||
if (mappedOpus) return mappedOpus
|
||||
|
||||
const provider = getAPIProvider()
|
||||
return provider === 'firstParty'
|
||||
? CLAUDE_OPUS_4_8_CONFIG.firstParty
|
||||
|
||||
Reference in New Issue
Block a user