diff --git a/package.json b/package.json index 828999ffe..14410b56c 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,8 @@ "@agentclientprotocol/sdk": "^0.19.0", "@claude-code-best/mcp-chrome-bridge": "^2.0.8", "ws": "^8.20.0", - "gray-matter": "^4.0.3" + "gray-matter": "^4.0.3", + "undici": "^7.24.6" }, "devDependencies": { "@alcalzone/ansi-tokenize": "^0.3.0", diff --git a/src/bootstrap/state.ts b/src/bootstrap/state.ts index 90d613b61..623d99f92 100644 --- a/src/bootstrap/state.ts +++ b/src/bootstrap/state.ts @@ -195,6 +195,8 @@ type State = { sdkBetas: string[] | undefined // Main thread agent type (from --agent flag or settings) mainThreadAgentType: string | undefined + // Currently active skill name (set when a /skill-name is invoked) + activeSkillName: string | undefined // Remote mode (--remote flag) isRemoteMode: boolean // Direct connect server URL (for display in header) @@ -381,6 +383,8 @@ function getInitialState(): State { sdkBetas: undefined, // Main thread agent type mainThreadAgentType: undefined, + // Currently active skill name + activeSkillName: undefined, // Remote mode isRemoteMode: false, ...(process.env.USER_TYPE === 'ant' @@ -1622,6 +1626,14 @@ export function setMainThreadAgentType(agentType: string | undefined): void { STATE.mainThreadAgentType = agentType } +export function getActiveSkillName(): string | undefined { + return STATE.activeSkillName +} + +export function setActiveSkillName(skillName: string | undefined): void { + STATE.activeSkillName = skillName +} + export function getIsRemoteMode(): boolean { return STATE.isRemoteMode } diff --git a/src/commands/clear/caches.ts b/src/commands/clear/caches.ts index 8eacc0d6f..479763b67 100644 --- a/src/commands/clear/caches.ts +++ b/src/commands/clear/caches.ts @@ -5,6 +5,7 @@ import { feature } from 'bun:bundle' import { clearInvokedSkills, + setActiveSkillName, setLastEmittedDate, } from '../../bootstrap/state.js' import { clearCommandsCache } from '../../commands.js' @@ -115,6 +116,8 @@ export function clearSessionCaches( if (!hasPreserved) clearAllDumpState() // Clear invoked skills cache (each entry holds full skill file content) clearInvokedSkills(preservedAgentIds) + // Reset active skill name so new session doesn't inherit previous session's agent-type + setActiveSkillName(undefined) // Clear git dir resolution cache clearResolveGitDirCache() // Clear dynamic skills (loaded from skill directories) diff --git a/src/constants/system.ts b/src/constants/system.ts index 0cd2e7652..0bac3c3d3 100644 --- a/src/constants/system.ts +++ b/src/constants/system.ts @@ -1,11 +1,6 @@ // Critical system constants extracted to break circular dependencies -import { feature } from 'bun:bundle' -import { getFeatureValue_CACHED_MAY_BE_STALE } from '../services/analytics/growthbook.js' -import { logForDebugging } from '../utils/debug.js' -import { isEnvDefinedFalsy } from '../utils/envUtils.js' import { getAPIProvider } from '../utils/model/providers.js' -import { getWorkload } from '../utils/workloadContext.js' const DEFAULT_PREFIX = `You are Claude Code, Anthropic's official CLI for Claude.` const AGENT_SDK_CLAUDE_CODE_PRESET_PREFIX = `You are Claude Code, Anthropic's official CLI for Claude, running within the Claude Agent SDK.` @@ -45,51 +40,6 @@ export function getCLISyspromptPrefix(options?: { return DEFAULT_PREFIX } -/** - * Check if attribution header is enabled. - * Enabled by default, can be disabled via env var or GrowthBook killswitch. - */ -function isAttributionHeaderEnabled(): boolean { - if (isEnvDefinedFalsy(process.env.CLAUDE_CODE_ATTRIBUTION_HEADER)) { - return false - } - return getFeatureValue_CACHED_MAY_BE_STALE('tengu_attribution_header', true) -} - -/** - * Get attribution header for API requests. - * Returns a header string with cc_version (including fingerprint) and cc_entrypoint. - * Enabled by default, can be disabled via env var or GrowthBook killswitch. - * - * When NATIVE_CLIENT_ATTESTATION is enabled, includes a `cch=00000` placeholder. - * Before the request is sent, Bun's native HTTP stack finds this placeholder - * in the request body and overwrites the zeros with a computed hash. The - * server verifies this token to confirm the request came from a real Claude - * Code client. See bun-anthropic/src/http/Attestation.zig for implementation. - * - * We use a placeholder (instead of injecting from Zig) because same-length - * replacement avoids Content-Length changes and buffer reallocation. - */ -export function getAttributionHeader(fingerprint: string): string { - if (!isAttributionHeaderEnabled()) { - return '' - } - - const version = `${MACRO.VERSION}.${fingerprint}` - const entrypoint = process.env.CLAUDE_CODE_ENTRYPOINT ?? 'unknown' - - // cch=00000 placeholder is overwritten by Bun's HTTP stack with attestation token - const cch = feature('NATIVE_CLIENT_ATTESTATION') ? ' cch=00000;' : '' - // cc_workload: turn-scoped hint so the API can route e.g. cron-initiated - // requests to a lower QoS pool. Absent = interactive default. Safe re: - // fingerprint (computed from msg chars + version only, line 78 above) and - // cch attestation (placeholder overwritten in serialized body bytes after - // this string is built). Server _parse_cc_header tolerates unknown extra - // fields so old API deploys silently ignore this. - const workload = getWorkload() - const workloadPair = workload ? ` cc_workload=${workload};` : '' - const header = `x-anthropic-billing-header: cc_version=${version}; cc_entrypoint=${entrypoint};${cch}${workloadPair}` - - logForDebugging(`attribution header ${header}`) - return header +export function getAttributionHeader(_fingerprint: string): string { + return '' } diff --git a/src/costrict/provider/fetch.ts b/src/costrict/provider/fetch.ts index c974749e5..94f6881c7 100644 --- a/src/costrict/provider/fetch.ts +++ b/src/costrict/provider/fetch.ts @@ -38,6 +38,15 @@ type CoStrictFetch = typeof fetch & { preconnect?: (url: string | URL) => void } +// PascalCase / camelCase → kebab-case: "StrictSpec" → "strict-spec", "TDD" → "tdd" +function toKebabCase(s: string | undefined): string | undefined { + if (!s) return undefined + return s + .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2') + .replace(/([a-z\d])([A-Z])/g, '$1-$2') + .toLowerCase() +} + /** * 创建自定义 fetch 函数,用于 CoStrict API 请求 * @@ -47,7 +56,10 @@ type CoStrictFetch = typeof fetch & { * 3. 注入 Authorization 和 CoStrict 特有 headers * 4. 反应性 401 错误恢复(自动重试一次) */ -export function createCoStrictFetch(): CoStrictFetch { +export function createCoStrictFetch(options?: { + agentType?: string +}): CoStrictFetch { + const agentType = toKebabCase(options?.agentType) || 'build' const costrictFetch = async ( input: RequestInfo | URL, init?: RequestInit, @@ -94,6 +106,7 @@ export function createCoStrictFetch(): CoStrictFetch { headers.set('X-Title', 'CoStrict-CLI') headers.set('X-Costrict-Version', `costrict-cli-${VERSION}`) headers.set('X-Request-ID', randomUUID()) + headers.set('agent-type', agentType) headers.set('zgsm-client-id', creds.machine_id) headers.set('zgsm-client-ide', 'cli') diff --git a/src/costrict/provider/index.ts b/src/costrict/provider/index.ts index 0db3ebe0a..8eb2bbe5f 100644 --- a/src/costrict/provider/index.ts +++ b/src/costrict/provider/index.ts @@ -33,6 +33,8 @@ import { resolveCoStrictModel } from './modelMapping.js' import { getCoStrictBaseURL } from './auth.js' import { loadCoStrictCredentials } from './credentials.js' import { isOpenAIThinkingEnabled } from '../../services/api/openai/requestBody.js' +import { fetchCoStrictModels } from './models.js' +import { getMainThreadAgentType, getActiveSkillName } from '../../bootstrap/state.js' /** * CoStrict 查询路径 @@ -57,10 +59,28 @@ export async function* queryModelCoStrict( const baseUrl = getCoStrictBaseURL(creds?.base_url) const chatBaseURL = `${baseUrl}/chat-rag/api/v1` - // 3. 规范化消息 + // 3. 从模型列表获取 maxTokens 相关参数 + let maxTokensParamKey: string = 'max_tokens' + let maxTokensValue: number | undefined + if (creds?.access_token) { + try { + const modelList = await fetchCoStrictModels(baseUrl, creds.access_token) + const modelInfo = modelList.find(m => m.id === costrictModel) + if (modelInfo) { + maxTokensParamKey = modelInfo.maxTokensKey || 'max_tokens' + if (modelInfo.maxTokens != null) { + maxTokensValue = modelInfo.maxTokens + } + } + } catch { + // 获取模型列表失败,使用默认值 + } + } + + // 4. 规范化消息 const messagesForAPI = normalizeMessagesForAPI(messages, tools) - // 4. 构建工具 schema + // 5. 构建工具 schema const toolSchemas = await Promise.all( tools.map(tool => toolToAPISchema(tool, { @@ -81,7 +101,7 @@ export async function* queryModelCoStrict( }, ) - // 5. 转换为 OpenAI 格式 + // 6. 转换为 OpenAI 格式 // 根据模型名称自动检测是否启用thinking模式 const enableThinking = isOpenAIThinkingEnabled(costrictModel) const openaiMessages = anthropicMessagesToOpenAI( @@ -92,8 +112,10 @@ export async function* queryModelCoStrict( const openaiTools = anthropicToolsToOpenAI(standardTools) const openaiToolChoice = anthropicToolChoiceToOpenAI(options.toolChoice) - // 6. 创建专用的 CoStrict OpenAI 客户端(不缓存,每次使用新的 fetch) - const costrictFetch = createCoStrictFetch() + // 7. 创建专用的 CoStrict OpenAI 客户端(不缓存,每次使用新的 fetch) + const costrictFetch = createCoStrictFetch({ + agentType: getMainThreadAgentType() ?? getActiveSkillName(), + }) const client = new OpenAI({ apiKey: 'costrict-managed', // 实际 token 由 createCoStrictFetch 注入 baseURL: chatBaseURL, @@ -111,28 +133,32 @@ export async function* queryModelCoStrict( `[CoStrict] model=${costrictModel}, baseURL=${chatBaseURL}, messages=${openaiMessages.length}, tools=${openaiTools.length}`, ) - // 7. 调用 API(流式) + // 8. 调用 API(流式) + const requestBody: Record = { + model: costrictModel, + messages: openaiMessages, + ...(openaiTools.length > 0 && { + tools: openaiTools, + ...(openaiToolChoice && { + tool_choice: + openaiToolChoice as OpenAI.Chat.Completions.ChatCompletionToolChoiceOption, + }), + }), + stream: true, + stream_options: { include_usage: true }, + ...(options.temperatureOverride !== undefined && { + temperature: options.temperatureOverride, + }), + ...(maxTokensValue != null && { + [maxTokensParamKey]: maxTokensValue, + }), + } const stream = await client.chat.completions.create( - { - model: costrictModel, - messages: openaiMessages, - ...(openaiTools.length > 0 && { - tools: openaiTools, - ...(openaiToolChoice && { - tool_choice: - openaiToolChoice as OpenAI.Chat.Completions.ChatCompletionToolChoiceOption, - }), - }), - stream: true, - stream_options: { include_usage: true }, - ...(options.temperatureOverride !== undefined && { - temperature: options.temperatureOverride, - }), - }, + requestBody as unknown as OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming, { signal }, ) - // 8. 转换流并 yield 事件 + // 9. 转换流并 yield 事件 const adaptedStream = adaptOpenAIStreamToAnthropic(stream, costrictModel) const contentBlocks: Record = {} diff --git a/src/costrict/provider/models.ts b/src/costrict/provider/models.ts index 16743ba5d..a63e68e11 100644 --- a/src/costrict/provider/models.ts +++ b/src/costrict/provider/models.ts @@ -12,6 +12,7 @@ export interface CoStrictModel { supportsImages?: boolean contextWindow?: number maxTokens?: number + maxTokensKey?: string creditConsumption?: number creditDiscount?: number [key: string]: any @@ -42,6 +43,7 @@ export async function fetchCoStrictModels( headers: { Authorization: `Bearer ${accessToken}`, Accept: 'application/json', + 'User-Agent': `csc/${MACRO.VERSION}`, }, }) diff --git a/src/utils/processUserInput/processSlashCommand.tsx b/src/utils/processUserInput/processSlashCommand.tsx index b2bf58965..59e4ceecf 100644 --- a/src/utils/processUserInput/processSlashCommand.tsx +++ b/src/utils/processUserInput/processSlashCommand.tsx @@ -25,7 +25,7 @@ import type { ProgressMessage, UserMessage, } from 'src/types/message.js' -import { addInvokedSkill, getSessionId } from '../../bootstrap/state.js' +import { addInvokedSkill, getSessionId, setActiveSkillName } from '../../bootstrap/state.js' import { COMMAND_MESSAGE_TAG, COMMAND_NAME_TAG } from '../../constants/xml.js' import type { CanUseToolFn } from '../../hooks/useCanUseTool.js' import { @@ -162,6 +162,8 @@ async function executeForkedSlashCommand( ? { ...baseAgent, effort: command.effort } : baseAgent + setActiveSkillName(agentDefinition.agentType ?? command.name) + logForDebugging( `Executing forked slash command /${command.name} with agent ${agentDefinition.agentType}`, ) @@ -1230,6 +1232,7 @@ async function getMessagesForPromptSlashCommand( skillContent, getAgentContext()?.agentId ?? null, ) + setActiveSkillName(command.name) const metadata = formatCommandLoadingMetadata(command, args)