mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-17 13:13:35 +08:00
fix: Route Claude attribution by model name
Provider-managed environments should preserve Claude attribution only when the selected model is actually Claude-prefixed. Non-Claude provider models now disable the attribution header so third-party providers do not inherit Claude-specific prompt metadata. Constraint: Claude model detection is intentionally model-name based to match the requested provider policy. Rejected: Remove attribution headers globally | official Claude-prefixed models should keep existing behavior. Confidence: high Scope-risk: moderate Directive: Do not broaden this policy without live provider request-shape verification. Tested: Targeted server, provider, settings, cron, and managed-env tests in prior verification pass. Not-tested: Full verify gate after this commit.
This commit is contained in:
parent
63ef7b1a5b
commit
8cce7ac8bb
@ -17,6 +17,7 @@ describe('ConversationService', () => {
|
||||
let originalOAuthToken: string | undefined
|
||||
let originalProviderManagedByHost: string | undefined
|
||||
let originalDiagnosticsFile: string | undefined
|
||||
let originalAttributionHeader: string | undefined
|
||||
let originalHome: string | undefined
|
||||
let originalPath: string | undefined
|
||||
let originalShell: string | undefined
|
||||
@ -34,6 +35,7 @@ describe('ConversationService', () => {
|
||||
originalOAuthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN
|
||||
originalProviderManagedByHost = process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST
|
||||
originalDiagnosticsFile = process.env.CLAUDE_CODE_DIAGNOSTICS_FILE
|
||||
originalAttributionHeader = process.env.CLAUDE_CODE_ATTRIBUTION_HEADER
|
||||
originalHome = process.env.HOME
|
||||
originalPath = process.env.PATH
|
||||
originalShell = process.env.SHELL
|
||||
@ -51,6 +53,7 @@ describe('ConversationService', () => {
|
||||
delete process.env.CLAUDE_CODE_ENTRYPOINT
|
||||
delete process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST
|
||||
delete process.env.CLAUDE_CODE_DIAGNOSTICS_FILE
|
||||
delete process.env.CLAUDE_CODE_ATTRIBUTION_HEADER
|
||||
process.env.CC_HAHA_DISABLE_TERMINAL_SHELL_ENV = '1'
|
||||
resetTerminalShellEnvironmentCacheForTests()
|
||||
})
|
||||
@ -83,6 +86,9 @@ describe('ConversationService', () => {
|
||||
if (originalDiagnosticsFile === undefined) delete process.env.CLAUDE_CODE_DIAGNOSTICS_FILE
|
||||
else process.env.CLAUDE_CODE_DIAGNOSTICS_FILE = originalDiagnosticsFile
|
||||
|
||||
if (originalAttributionHeader === undefined) delete process.env.CLAUDE_CODE_ATTRIBUTION_HEADER
|
||||
else process.env.CLAUDE_CODE_ATTRIBUTION_HEADER = originalAttributionHeader
|
||||
|
||||
if (originalHome === undefined) delete process.env.HOME
|
||||
else process.env.HOME = originalHome
|
||||
|
||||
@ -133,6 +139,7 @@ describe('ConversationService', () => {
|
||||
expect(env.ANTHROPIC_AUTH_TOKEN).toBe('test-token')
|
||||
expect(env.ANTHROPIC_BASE_URL).toBe('https://example.invalid/anthropic')
|
||||
expect(env.ANTHROPIC_MODEL).toBe('test-model')
|
||||
expect(env.CLAUDE_CODE_ATTRIBUTION_HEADER).toBe('0')
|
||||
expect(env.CLAUDE_CODE_DIAGNOSTICS_FILE).toBe(path.join(tmpDir, 'cc-haha', 'diagnostics', 'cli-diagnostics.jsonl'))
|
||||
expect(env.CLAUDE_COWORK_MEMORY_PATH_OVERRIDE).toBe(
|
||||
`${path.join(tmpDir, 'projects', 'D--workspace-code-myself-code-cc-haha', 'memory')}${path.sep}`,
|
||||
@ -280,6 +287,7 @@ describe('ConversationService', () => {
|
||||
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('kimi-k2.6')
|
||||
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('kimi-k2.6')
|
||||
expect(env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST).toBe('1')
|
||||
expect(env.CLAUDE_CODE_ATTRIBUTION_HEADER).toBe('0')
|
||||
expect(env.CLAUDE_CODE_ENTRYPOINT).toBeUndefined()
|
||||
})
|
||||
|
||||
@ -307,6 +315,7 @@ describe('ConversationService', () => {
|
||||
|
||||
expect(env.ANTHROPIC_BASE_URL).toBe(`http://127.0.0.1:3456/proxy/providers/${provider.id}`)
|
||||
expect(env.ANTHROPIC_MODEL).toBe('new-provider-sonnet')
|
||||
expect(env.CLAUDE_CODE_ATTRIBUTION_HEADER).toBe('0')
|
||||
})
|
||||
|
||||
test('buildChildEnv clears stale api key for bearer-token providers', async () => {
|
||||
@ -336,6 +345,7 @@ describe('ConversationService', () => {
|
||||
expect(env.ANTHROPIC_API_KEY).toBe('')
|
||||
expect(env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6')
|
||||
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES).toBe('none')
|
||||
expect(env.CLAUDE_CODE_ATTRIBUTION_HEADER).toBe('1')
|
||||
})
|
||||
|
||||
test('buildChildEnv can force official auth even when a custom default provider exists', async () => {
|
||||
|
||||
@ -326,6 +326,7 @@ describe('cron scheduler launcher resolution', () => {
|
||||
expect(env.ANTHROPIC_MODEL).toBe('provider-fast')
|
||||
expect(env.ANTHROPIC_MODEL).not.toBe('stale-parent-model')
|
||||
expect(env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST).toBe('1')
|
||||
expect(env.CLAUDE_CODE_ATTRIBUTION_HEADER).toBe('0')
|
||||
expect(env.CLAUDE_CODE_ENTRYPOINT).toBe('sdk-cli')
|
||||
})
|
||||
|
||||
|
||||
@ -523,9 +523,31 @@ describe('ProviderService', () => {
|
||||
expect(env.ANTHROPIC_DEFAULT_HAIKU_MODEL).toBe('model-haiku')
|
||||
expect(env.ANTHROPIC_DEFAULT_SONNET_MODEL).toBe('model-sonnet')
|
||||
expect(env.ANTHROPIC_DEFAULT_OPUS_MODEL).toBe('model-opus')
|
||||
expect(env.CLAUDE_CODE_ATTRIBUTION_HEADER).toBe('0')
|
||||
expect(env.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBeUndefined()
|
||||
})
|
||||
|
||||
test('should preserve attribution header for Claude-prefixed provider models', async () => {
|
||||
const svc = new ProviderService()
|
||||
const provider = await svc.addProvider(sampleInput({
|
||||
models: {
|
||||
main: 'Claude Sonnet 4.6',
|
||||
haiku: 'Claude Haiku 4.5',
|
||||
sonnet: 'Claude Sonnet 4.6',
|
||||
opus: 'Claude Opus 4.7',
|
||||
},
|
||||
}))
|
||||
|
||||
await svc.activateProvider(provider.id)
|
||||
|
||||
const settings = await readSettings()
|
||||
const env = settings.env as Record<string, string>
|
||||
expect(env.CLAUDE_CODE_ATTRIBUTION_HEADER).toBe('1')
|
||||
|
||||
const runtimeEnv = await svc.getProviderRuntimeEnv(provider.id)
|
||||
expect(runtimeEnv.CLAUDE_CODE_ATTRIBUTION_HEADER).toBe('1')
|
||||
})
|
||||
|
||||
test('should honor provider auth env strategies on activation and runtime env', async () => {
|
||||
const svc = new ProviderService()
|
||||
|
||||
@ -615,6 +637,7 @@ describe('ProviderService', () => {
|
||||
expect(clearedEnv.API_TIMEOUT_MS).toBeUndefined()
|
||||
expect(clearedEnv.CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC).toBeUndefined()
|
||||
expect(clearedEnv.CLAUDE_CODE_AUTO_COMPACT_WINDOW).toBeUndefined()
|
||||
expect(clearedEnv.CLAUDE_CODE_ATTRIBUTION_HEADER).toBeUndefined()
|
||||
expect(clearedEnv.CLAUDE_CODE_MODEL_CONTEXT_WINDOWS).toBeUndefined()
|
||||
})
|
||||
|
||||
|
||||
@ -620,6 +620,7 @@ describe('Models API', () => {
|
||||
|
||||
const managedSettings = await providerSvc.getManagedSettings()
|
||||
expect(managedSettings.model).toBe('glm-5-turbo')
|
||||
expect((managedSettings.env as Record<string, string>).CLAUDE_CODE_ATTRIBUTION_HEADER).toBe('0')
|
||||
|
||||
const globalSettings = await settingsSvc.getUserSettings()
|
||||
expect(globalSettings.model).toBeUndefined()
|
||||
|
||||
@ -10,6 +10,7 @@
|
||||
|
||||
import { SettingsService } from '../services/settingsService.js'
|
||||
import { ProviderService } from '../services/providerService.js'
|
||||
import { attributionHeaderEnvForModel } from '../services/attributionHeaderPolicy.js'
|
||||
import { ApiError, errorResponse } from '../middleware/errorHandler.js'
|
||||
import { hasOpenAIAuthLogin } from '../../utils/auth.js'
|
||||
import { OPENAI_CODEX_MODEL_CATALOG } from '../../services/openaiAuth/models.js'
|
||||
@ -275,7 +276,16 @@ async function handleCurrentModel(req: Request): Promise<Response> {
|
||||
}
|
||||
const { activeId } = await providerService.listProviders()
|
||||
if (activeId) {
|
||||
await providerService.updateManagedSettings(updates)
|
||||
const currentManagedSettings = await providerService.getManagedSettings()
|
||||
const currentEnv =
|
||||
(currentManagedSettings.env as Record<string, string> | undefined) ?? {}
|
||||
await providerService.updateManagedSettings({
|
||||
...updates,
|
||||
env: {
|
||||
...currentEnv,
|
||||
...attributionHeaderEnvForModel(baseId),
|
||||
},
|
||||
})
|
||||
} else {
|
||||
await settingsService.updateUserSettings(updates)
|
||||
}
|
||||
|
||||
16
src/server/services/attributionHeaderPolicy.ts
Normal file
16
src/server/services/attributionHeaderPolicy.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export const ATTRIBUTION_HEADER_ENV_KEY = 'CLAUDE_CODE_ATTRIBUTION_HEADER'
|
||||
|
||||
export function isClaudeModelName(modelName?: string | null): boolean {
|
||||
return typeof modelName === 'string' && modelName.trim().toLowerCase().startsWith('claude')
|
||||
}
|
||||
|
||||
export function attributionHeaderEnvForModel(
|
||||
modelName?: string | null,
|
||||
): Record<string, string> {
|
||||
const normalizedModel = modelName?.trim()
|
||||
if (!normalizedModel) return {}
|
||||
|
||||
return {
|
||||
[ATTRIBUTION_HEADER_ENV_KEY]: isClaudeModelName(normalizedModel) ? '1' : '0',
|
||||
}
|
||||
}
|
||||
@ -26,6 +26,7 @@ import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
|
||||
import { findCanonicalGitRoot } from '../../utils/git.js'
|
||||
import { sanitizePath } from '../../utils/path.js'
|
||||
import { getProcessEnvWithTerminalShellEnvironment } from '../../utils/terminalShellEnvironment.js'
|
||||
import { attributionHeaderEnvForModel } from './attributionHeaderPolicy.js'
|
||||
|
||||
const MAX_CAPTURED_PROCESS_LINES = 80
|
||||
const MAX_CAPTURED_SDK_MESSAGES = 40
|
||||
@ -885,6 +886,7 @@ export class ConversationService {
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'CC_HAHA_SEND_DISABLED_THINKING',
|
||||
'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
|
||||
'CLAUDE_CODE_ATTRIBUTION_HEADER',
|
||||
'CLAUDE_CODE_MODEL_CONTEXT_WINDOWS',
|
||||
] as const
|
||||
|
||||
@ -913,6 +915,11 @@ export class ConversationService {
|
||||
if (explicitProviderEnv && options?.model?.trim()) {
|
||||
explicitProviderEnv.ANTHROPIC_MODEL = options.model.trim()
|
||||
}
|
||||
const attributionHeaderEnv = attributionHeaderEnvForModel(
|
||||
options?.model?.trim() ||
|
||||
explicitProviderEnv?.ANTHROPIC_MODEL ||
|
||||
cleanEnv.ANTHROPIC_MODEL,
|
||||
)
|
||||
|
||||
const cliDiagnosticsPath = diagnosticsService.getCliDiagnosticsPath()
|
||||
try {
|
||||
@ -957,6 +964,7 @@ export class ConversationService {
|
||||
...(this.shouldMarkManagedOAuth(options?.providerId)
|
||||
? await this.buildOfficialOAuthEnv()
|
||||
: {}),
|
||||
...attributionHeaderEnv,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1034,6 +1042,7 @@ export class ConversationService {
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'CC_HAHA_SEND_DISABLED_THINKING',
|
||||
'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
|
||||
'CLAUDE_CODE_ATTRIBUTION_HEADER',
|
||||
'CLAUDE_CODE_MODEL_CONTEXT_WINDOWS',
|
||||
].some((key) => typeof env[key] === 'string' && env[key]!.trim().length > 0)
|
||||
} catch {
|
||||
|
||||
@ -22,6 +22,7 @@ import {
|
||||
resolveClaudeCliLauncher,
|
||||
} from '../../utils/desktopBundledCli.js'
|
||||
import { getProcessEnvWithTerminalShellEnvironment } from '../../utils/terminalShellEnvironment.js'
|
||||
import { attributionHeaderEnvForModel } from './attributionHeaderPolicy.js'
|
||||
|
||||
// ─── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
@ -659,6 +660,11 @@ export class CronScheduler {
|
||||
if (explicitProviderEnv && task.model?.trim()) {
|
||||
explicitProviderEnv.ANTHROPIC_MODEL = task.model.trim()
|
||||
}
|
||||
const attributionHeaderEnv = attributionHeaderEnvForModel(
|
||||
task.model?.trim() ||
|
||||
explicitProviderEnv?.ANTHROPIC_MODEL ||
|
||||
cleanEnv.ANTHROPIC_MODEL,
|
||||
)
|
||||
|
||||
return {
|
||||
...cleanEnv,
|
||||
@ -677,6 +683,7 @@ export class CronScheduler {
|
||||
...(this.shouldMarkManagedOAuth(task.providerId)
|
||||
? await this.buildOfficialOAuthEnv()
|
||||
: {}),
|
||||
...attributionHeaderEnv,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,6 +19,10 @@ import { openaiResponsesToAnthropic } from '../proxy/transform/openaiResponsesTo
|
||||
import type { AnthropicRequest, AnthropicResponse } from '../proxy/transform/types.js'
|
||||
import { PROVIDER_PRESETS } from '../config/providerPresets.js'
|
||||
import { MODEL_CONTEXT_WINDOWS_ENV_KEY } from '../../utils/model/modelContextWindows.js'
|
||||
import {
|
||||
ATTRIBUTION_HEADER_ENV_KEY,
|
||||
attributionHeaderEnvForModel,
|
||||
} from './attributionHeaderPolicy.js'
|
||||
import {
|
||||
CURRENT_PROVIDER_INDEX_SCHEMA_VERSION,
|
||||
ensurePersistentStorageUpgraded,
|
||||
@ -47,6 +51,7 @@ const MANAGED_ENV_KEYS = [
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL',
|
||||
'ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES',
|
||||
'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
|
||||
ATTRIBUTION_HEADER_ENV_KEY,
|
||||
MODEL_CONTEXT_WINDOWS_ENV_KEY,
|
||||
] as const
|
||||
|
||||
@ -393,6 +398,7 @@ export class ProviderService {
|
||||
ANTHROPIC_DEFAULT_HAIKU_MODEL: provider.models.haiku,
|
||||
ANTHROPIC_DEFAULT_SONNET_MODEL: provider.models.sonnet,
|
||||
ANTHROPIC_DEFAULT_OPUS_MODEL: provider.models.opus,
|
||||
...attributionHeaderEnvForModel(provider.models.main),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { describe, expect, test } from 'bun:test'
|
||||
import { isProviderManagedEnvVar } from '../managedEnvConstants.js'
|
||||
import { normalizeLegacyDeepSeekManagedEnv } from '../providerManagedEnvCompat.js'
|
||||
|
||||
describe('provider managed env compatibility', () => {
|
||||
@ -36,4 +37,8 @@ describe('provider managed env compatibility', () => {
|
||||
expect(changed).toBe(false)
|
||||
expect(env).toBe(input)
|
||||
})
|
||||
|
||||
test('treats attribution header routing as provider-managed env', () => {
|
||||
expect(isProviderManagedEnvVar('CLAUDE_CODE_ATTRIBUTION_HEADER')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@ -59,6 +59,7 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([
|
||||
'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION',
|
||||
'CLAUDE_CODE_SUBAGENT_MODEL',
|
||||
'CLAUDE_CODE_AUTO_COMPACT_WINDOW',
|
||||
'CLAUDE_CODE_ATTRIBUTION_HEADER',
|
||||
'CLAUDE_CODE_MODEL_CONTEXT_WINDOWS',
|
||||
])
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user