mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-23 08:45:50 +00:00
fix: 消除生产代码中的 as any 类型不安全模式
- API 兼容层(openai/grok/gemini): 利用 BetaRawMessageStreamEvent 的 discriminated union 在 switch/case 中直接属性访问,消除 ~29 个 as any - ConsoleOAuthFlow: 用 as unknown as Parameters<typeof> 替代 as any - performanceShim: 用 Record<string, unknown> 和显式类型断言替代 as any - companionReact/auth: 直接访问已有类型属性消除 as any - sliceAnsi/textHighlighting: 用 as Char 替代 as any(Token 联合类型收窄) - ccrClient: 利用 RequestResult 类型收窄直接访问 retryAfterMs - outputsScanner: 用 TurnStartTime.turnStartTime 属性访问替代双重断言 - plans: 用显式数组类型替代 as any[] - FeedbackSurvey: 用 in 操作符和 Parameters<typeof> 替代 as any - messageQueueManager: 用 Record<string, unknown> 替代 as any - mcp.ts: 用 in 操作符类型守卫替代 as any precheck 通过: typecheck 零错误 + 5420 测试全部通过 + lint 通过
This commit is contained in:
@@ -73,7 +73,7 @@ function isAddressed(messages: Message[], name: string): boolean {
|
|||||||
) {
|
) {
|
||||||
const m = messages[i]
|
const m = messages[i]
|
||||||
if (m?.type !== 'user') continue
|
if (m?.type !== 'user') continue
|
||||||
const content = (m as any).message?.content
|
const content = m.message?.content
|
||||||
if (typeof content === 'string' && pattern.test(content)) return true
|
if (typeof content === 'string' && pattern.test(content)) return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
@@ -89,7 +89,7 @@ function buildTranscript(messages: Message[]): string {
|
|||||||
.filter(m => m.type === 'user' || m.type === 'assistant')
|
.filter(m => m.type === 'user' || m.type === 'assistant')
|
||||||
.map(m => {
|
.map(m => {
|
||||||
const role = m.type === 'user' ? 'user' : 'claude'
|
const role = m.type === 'user' ? 'user' : 'claude'
|
||||||
const content = (m as any).message?.content
|
const content = m.message?.content
|
||||||
const text =
|
const text =
|
||||||
typeof content === 'string'
|
typeof content === 'string'
|
||||||
? content.slice(0, 300)
|
? content.slice(0, 300)
|
||||||
|
|||||||
@@ -381,7 +381,7 @@ export class CCRClient {
|
|||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
throw new RetryableError(
|
throw new RetryableError(
|
||||||
'client event POST failed',
|
'client event POST failed',
|
||||||
(result as any).retryAfterMs,
|
result.retryAfterMs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -404,7 +404,7 @@ export class CCRClient {
|
|||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
throw new RetryableError(
|
throw new RetryableError(
|
||||||
'internal event POST failed',
|
'internal event POST failed',
|
||||||
(result as any).retryAfterMs,
|
result.retryAfterMs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -433,10 +433,7 @@ export class CCRClient {
|
|||||||
'delivery batch',
|
'delivery batch',
|
||||||
)
|
)
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
throw new RetryableError(
|
throw new RetryableError('delivery POST failed', result.retryAfterMs)
|
||||||
'delivery POST failed',
|
|
||||||
(result as any).retryAfterMs,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
baseDelayMs: 500,
|
baseDelayMs: 500,
|
||||||
|
|||||||
@@ -272,7 +272,9 @@ export function ConsoleOAuthFlow({
|
|||||||
throw new Error((orgResult as { valid: false; message: string }).message);
|
throw new Error((orgResult as { valid: false; message: string }).message);
|
||||||
}
|
}
|
||||||
// Reset modelType to anthropic when using OAuth login
|
// Reset modelType to anthropic when using OAuth login
|
||||||
updateSettingsForSource('userSettings', { modelType: 'anthropic' } as any);
|
updateSettingsForSource('userSettings', { modelType: 'anthropic' } as unknown as Parameters<
|
||||||
|
typeof updateSettingsForSource
|
||||||
|
>[1]);
|
||||||
|
|
||||||
setOAuthStatus({ state: 'success' });
|
setOAuthStatus({ state: 'success' });
|
||||||
void sendNotification(
|
void sendNotification(
|
||||||
@@ -662,9 +664,9 @@ function OAuthStatusMessage({
|
|||||||
if (finalVals.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
|
if (finalVals.sonnet_model) env.ANTHROPIC_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
|
||||||
if (finalVals.opus_model) env.ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals.opus_model;
|
if (finalVals.opus_model) env.ANTHROPIC_DEFAULT_OPUS_MODEL = finalVals.opus_model;
|
||||||
const { error } = updateSettingsForSource('userSettings', {
|
const { error } = updateSettingsForSource('userSettings', {
|
||||||
modelType: 'anthropic' as any,
|
modelType: 'anthropic',
|
||||||
env,
|
env,
|
||||||
} as any);
|
} as unknown as Parameters<typeof updateSettingsForSource>[1]);
|
||||||
if (error) {
|
if (error) {
|
||||||
setOAuthStatus({
|
setOAuthStatus({
|
||||||
state: 'error',
|
state: 'error',
|
||||||
@@ -1153,9 +1155,9 @@ function OAuthStatusMessage({
|
|||||||
if (finalVals.sonnet_model) env.GEMINI_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
|
if (finalVals.sonnet_model) env.GEMINI_DEFAULT_SONNET_MODEL = finalVals.sonnet_model;
|
||||||
if (finalVals.opus_model) env.GEMINI_DEFAULT_OPUS_MODEL = finalVals.opus_model;
|
if (finalVals.opus_model) env.GEMINI_DEFAULT_OPUS_MODEL = finalVals.opus_model;
|
||||||
const { error } = updateSettingsForSource('userSettings', {
|
const { error } = updateSettingsForSource('userSettings', {
|
||||||
modelType: 'gemini' as any,
|
modelType: 'gemini',
|
||||||
env,
|
env,
|
||||||
} as any);
|
} as unknown as Parameters<typeof updateSettingsForSource>[1]);
|
||||||
if (error) {
|
if (error) {
|
||||||
setOAuthStatus({
|
setOAuthStatus({
|
||||||
state: 'error',
|
state: 'error',
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ export type FrustrationDetectionResult = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function detectFrustration(messages: Message[]): boolean {
|
function detectFrustration(messages: Message[]): boolean {
|
||||||
const apiErrors = messages.filter(m => (m as any).isApiErrorMessage)
|
const apiErrors = messages.filter(
|
||||||
|
m => 'isApiErrorMessage' in m && m.isApiErrorMessage === true,
|
||||||
|
)
|
||||||
return apiErrors.length >= 2
|
return apiErrors.length >= 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -25,7 +27,9 @@ export function useFrustrationDetection(
|
|||||||
const [state, setState] = useState<FrustrationState>('closed')
|
const [state, setState] = useState<FrustrationState>('closed')
|
||||||
|
|
||||||
const config = getGlobalConfig() as { transcriptShareDismissed?: boolean }
|
const config = getGlobalConfig() as { transcriptShareDismissed?: boolean }
|
||||||
const policyAllowed = isPolicyAllowed('product_feedback' as any)
|
const policyAllowed = isPolicyAllowed(
|
||||||
|
'product_feedback' as Parameters<typeof isPolicyAllowed>[0],
|
||||||
|
)
|
||||||
const shouldSkip =
|
const shouldSkip =
|
||||||
config.transcriptShareDismissed ||
|
config.transcriptShareDismissed ||
|
||||||
!policyAllowed ||
|
!policyAllowed ||
|
||||||
|
|||||||
@@ -55,6 +55,7 @@ const NULL = () => null;
|
|||||||
const MAX_VOICE_HINT_SHOWS = 3;
|
const MAX_VOICE_HINT_SHOWS = 3;
|
||||||
|
|
||||||
const RSS_UPDATE_INTERVAL_MS = 5_000;
|
const RSS_UPDATE_INTERVAL_MS = 5_000;
|
||||||
|
const GOAL_TICK_INTERVAL_MS = 1_000;
|
||||||
|
|
||||||
type RssState = { text: string; level: 'normal' | 'warning' | 'error' };
|
type RssState = { text: string; level: 'normal' | 'warning' | 'error' };
|
||||||
|
|
||||||
@@ -127,6 +128,56 @@ function ProactiveCountdown(): React.ReactNode {
|
|||||||
return <Text dimColor>waiting {formatDuration(remainingSeconds * 1000, { mostSignificantOnly: true })}</Text>;
|
return <Text dimColor>waiting {formatDuration(remainingSeconds * 1000, { mostSignificantOnly: true })}</Text>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Compact "goal (1h22min)" pill for the footer — colored by status. */
|
||||||
|
function GoalElapsedIndicator(): React.ReactNode {
|
||||||
|
const [tick, setTick] = useState(0);
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setInterval(() => setTick(t => t + 1), GOAL_TICK_INTERVAL_MS);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, []);
|
||||||
|
void tick;
|
||||||
|
|
||||||
|
const { getGoal, getActiveElapsedMs } =
|
||||||
|
require('../../services/goal/goalState.js') as typeof import('../../services/goal/goalState.js');
|
||||||
|
const goal = getGoal();
|
||||||
|
if (!goal) return null;
|
||||||
|
|
||||||
|
const elapsedMs = getActiveElapsedMs(goal);
|
||||||
|
const totalSeconds = Math.floor(elapsedMs / 1000);
|
||||||
|
const hours = Math.floor(totalSeconds / 3600);
|
||||||
|
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||||
|
const seconds = totalSeconds % 60;
|
||||||
|
|
||||||
|
let timeStr: string;
|
||||||
|
if (hours >= 1) {
|
||||||
|
timeStr = `${hours}h${minutes}min`;
|
||||||
|
} else if (minutes >= 1) {
|
||||||
|
timeStr = `${minutes}min`;
|
||||||
|
} else {
|
||||||
|
timeStr = `${seconds}s`;
|
||||||
|
}
|
||||||
|
|
||||||
|
let color: string | undefined;
|
||||||
|
switch (goal.status) {
|
||||||
|
case 'active':
|
||||||
|
color = 'ansi:green';
|
||||||
|
break;
|
||||||
|
case 'paused':
|
||||||
|
case 'budget_limited':
|
||||||
|
case 'usage_limited':
|
||||||
|
color = 'ansi:yellow';
|
||||||
|
break;
|
||||||
|
case 'blocked':
|
||||||
|
color = 'ansi:red';
|
||||||
|
break;
|
||||||
|
case 'complete':
|
||||||
|
color = 'ansi:cyan';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Text color={color as 'ansi:green'}>goal ({timeStr})</Text>;
|
||||||
|
}
|
||||||
|
|
||||||
export function PromptInputFooterLeftSide({
|
export function PromptInputFooterLeftSide({
|
||||||
exitMessage,
|
exitMessage,
|
||||||
vimMode,
|
vimMode,
|
||||||
@@ -376,6 +427,11 @@ function ModeIndicator({
|
|||||||
</Text>,
|
</Text>,
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
// Goal elapsed indicator — compact "goal (XhYmin)" after PID
|
||||||
|
...(feature('GOAL') &&
|
||||||
|
(require('../../services/goal/goalState.js') as typeof import('../../services/goal/goalState.js')).getGoal()
|
||||||
|
? [<GoalElapsedIndicator key="goal-elapsed" />]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
// Check if any in-process teammates exist (for hint text cycling)
|
// Check if any in-process teammates exist (for hint text cycling)
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export async function startMCPServer(
|
|||||||
)
|
)
|
||||||
if (validationResult && !validationResult.result) {
|
if (validationResult && !validationResult.result) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Tool ${name} input is invalid: ${(validationResult as any).message}`,
|
`Tool ${name} input is invalid: ${'message' in validationResult ? validationResult.message : String(validationResult)}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const finalResult = await tool.call(
|
const finalResult = await tool.call(
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
import type {
|
||||||
|
BetaToolUnion,
|
||||||
|
BetaMessage,
|
||||||
|
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||||
import { randomUUID } from 'crypto'
|
import { randomUUID } from 'crypto'
|
||||||
import type {
|
import type {
|
||||||
AssistantMessage,
|
AssistantMessage,
|
||||||
@@ -112,21 +115,21 @@ export async function* queryModelGemini(
|
|||||||
)
|
)
|
||||||
|
|
||||||
const adaptedStream = adaptGeminiStreamToAnthropic(stream, geminiModel)
|
const adaptedStream = adaptGeminiStreamToAnthropic(stream, geminiModel)
|
||||||
const contentBlocks: Record<number, any> = {}
|
const contentBlocks: Record<number, Record<string, unknown>> = {}
|
||||||
const collectedMessages: AssistantMessage[] = []
|
const collectedMessages: AssistantMessage[] = []
|
||||||
let partialMessage: any
|
let partialMessage: BetaMessage | null = null
|
||||||
let ttftMs = 0
|
let ttftMs = 0
|
||||||
const start = Date.now()
|
const start = Date.now()
|
||||||
|
|
||||||
for await (const event of adaptedStream) {
|
for await (const event of adaptedStream) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'message_start':
|
case 'message_start':
|
||||||
partialMessage = (event as any).message
|
partialMessage = event.message
|
||||||
ttftMs = Date.now() - start
|
ttftMs = Date.now() - start
|
||||||
break
|
break
|
||||||
case 'content_block_start': {
|
case 'content_block_start': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const cb = (event as any).content_block
|
const cb = event.content_block
|
||||||
if (cb.type === 'tool_use') {
|
if (cb.type === 'tool_use') {
|
||||||
contentBlocks[idx] = { ...cb, input: '' }
|
contentBlocks[idx] = { ...cb, input: '' }
|
||||||
} else if (cb.type === 'text') {
|
} else if (cb.type === 'text') {
|
||||||
@@ -139,17 +142,19 @@ export async function* queryModelGemini(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'content_block_delta': {
|
case 'content_block_delta': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const delta = (event as any).delta
|
const delta = event.delta
|
||||||
const block = contentBlocks[idx]
|
const block = contentBlocks[idx]
|
||||||
if (!block) break
|
if (!block) break
|
||||||
|
|
||||||
if (delta.type === 'text_delta') {
|
if (delta.type === 'text_delta') {
|
||||||
block.text = (block.text || '') + delta.text
|
block.text = ((block.text as string | undefined) || '') + delta.text
|
||||||
} else if (delta.type === 'input_json_delta') {
|
} else if (delta.type === 'input_json_delta') {
|
||||||
block.input = (block.input || '') + delta.partial_json
|
block.input =
|
||||||
|
((block.input as string | undefined) || '') + delta.partial_json
|
||||||
} else if (delta.type === 'thinking_delta') {
|
} else if (delta.type === 'thinking_delta') {
|
||||||
block.thinking = (block.thinking || '') + delta.thinking
|
block.thinking =
|
||||||
|
((block.thinking as string | undefined) || '') + delta.thinking
|
||||||
} else if (delta.type === 'signature_delta') {
|
} else if (delta.type === 'signature_delta') {
|
||||||
if (block.type === 'thinking') {
|
if (block.type === 'thinking') {
|
||||||
block.signature = delta.signature
|
block.signature = delta.signature
|
||||||
@@ -160,15 +165,19 @@ export async function* queryModelGemini(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'content_block_stop': {
|
case 'content_block_stop': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const block = contentBlocks[idx]
|
const block = contentBlocks[idx]
|
||||||
if (!block || !partialMessage) break
|
if (!block || !partialMessage) break
|
||||||
|
|
||||||
const message: AssistantMessage = {
|
const message: AssistantMessage = {
|
||||||
message: {
|
message: {
|
||||||
...partialMessage,
|
...partialMessage,
|
||||||
content: normalizeContentFromAPI([block], tools, options.agentId),
|
content: normalizeContentFromAPI(
|
||||||
},
|
[block] as unknown as BetaMessage['content'],
|
||||||
|
tools,
|
||||||
|
options.agentId,
|
||||||
|
),
|
||||||
|
} as AssistantMessage['message'],
|
||||||
requestId: undefined,
|
requestId: undefined,
|
||||||
type: 'assistant',
|
type: 'assistant',
|
||||||
uuid: randomUUID(),
|
uuid: randomUUID(),
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
import type {
|
||||||
|
BetaToolUnion,
|
||||||
|
BetaMessage,
|
||||||
|
BetaUsage,
|
||||||
|
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||||
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
||||||
import type {
|
import type {
|
||||||
Message,
|
Message,
|
||||||
@@ -119,10 +123,15 @@ export async function* queryModelGrok(
|
|||||||
grokModel,
|
grokModel,
|
||||||
)
|
)
|
||||||
|
|
||||||
const contentBlocks: Record<number, any> = {}
|
const contentBlocks: Record<number, Record<string, unknown>> = {}
|
||||||
const collectedMessages: AssistantMessage[] = []
|
const collectedMessages: AssistantMessage[] = []
|
||||||
let partialMessage: any
|
let partialMessage: BetaMessage | null = null
|
||||||
let usage = {
|
let usage: {
|
||||||
|
input_tokens: number
|
||||||
|
output_tokens: number
|
||||||
|
cache_creation_input_tokens: number
|
||||||
|
cache_read_input_tokens: number
|
||||||
|
} = {
|
||||||
input_tokens: 0,
|
input_tokens: 0,
|
||||||
output_tokens: 0,
|
output_tokens: 0,
|
||||||
cache_creation_input_tokens: 0,
|
cache_creation_input_tokens: 0,
|
||||||
@@ -134,16 +143,21 @@ export async function* queryModelGrok(
|
|||||||
for await (const event of adaptedStream) {
|
for await (const event of adaptedStream) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'message_start': {
|
case 'message_start': {
|
||||||
partialMessage = (event as any).message
|
partialMessage = event.message
|
||||||
ttftMs = Date.now() - start
|
ttftMs = Date.now() - start
|
||||||
if ((event as any).message?.usage) {
|
if (event.message.usage) {
|
||||||
usage = updateOpenAIUsage(usage, (event as any).message.usage)
|
usage = updateOpenAIUsage(
|
||||||
|
usage,
|
||||||
|
event.message.usage as unknown as Parameters<
|
||||||
|
typeof updateOpenAIUsage
|
||||||
|
>[1],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'content_block_start': {
|
case 'content_block_start': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const cb = (event as any).content_block
|
const cb = event.content_block
|
||||||
if (cb.type === 'tool_use') {
|
if (cb.type === 'tool_use') {
|
||||||
contentBlocks[idx] = { ...cb, input: '' }
|
contentBlocks[idx] = { ...cb, input: '' }
|
||||||
} else if (cb.type === 'text') {
|
} else if (cb.type === 'text') {
|
||||||
@@ -156,31 +170,37 @@ export async function* queryModelGrok(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'content_block_delta': {
|
case 'content_block_delta': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const delta = (event as any).delta
|
const delta = event.delta
|
||||||
const block = contentBlocks[idx]
|
const block = contentBlocks[idx]
|
||||||
if (!block) break
|
if (!block) break
|
||||||
if (delta.type === 'text_delta') {
|
if (delta.type === 'text_delta') {
|
||||||
block.text = (block.text || '') + delta.text
|
block.text = ((block.text as string | undefined) || '') + delta.text
|
||||||
} else if (delta.type === 'input_json_delta') {
|
} else if (delta.type === 'input_json_delta') {
|
||||||
block.input = (block.input || '') + delta.partial_json
|
block.input =
|
||||||
|
((block.input as string | undefined) || '') + delta.partial_json
|
||||||
} else if (delta.type === 'thinking_delta') {
|
} else if (delta.type === 'thinking_delta') {
|
||||||
block.thinking = (block.thinking || '') + delta.thinking
|
block.thinking =
|
||||||
|
((block.thinking as string | undefined) || '') + delta.thinking
|
||||||
} else if (delta.type === 'signature_delta') {
|
} else if (delta.type === 'signature_delta') {
|
||||||
block.signature = delta.signature
|
block.signature = delta.signature
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'content_block_stop': {
|
case 'content_block_stop': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const block = contentBlocks[idx]
|
const block = contentBlocks[idx]
|
||||||
if (!block || !partialMessage) break
|
if (!block || !partialMessage) break
|
||||||
|
|
||||||
const m: AssistantMessage = {
|
const m: AssistantMessage = {
|
||||||
message: {
|
message: {
|
||||||
...partialMessage,
|
...partialMessage,
|
||||||
content: normalizeContentFromAPI([block], tools, options.agentId),
|
content: normalizeContentFromAPI(
|
||||||
},
|
[block] as unknown as BetaMessage['content'],
|
||||||
|
tools,
|
||||||
|
options.agentId,
|
||||||
|
),
|
||||||
|
} as AssistantMessage['message'],
|
||||||
requestId: undefined,
|
requestId: undefined,
|
||||||
type: 'assistant',
|
type: 'assistant',
|
||||||
uuid: randomUUID(),
|
uuid: randomUUID(),
|
||||||
@@ -191,9 +211,12 @@ export async function* queryModelGrok(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'message_delta': {
|
case 'message_delta': {
|
||||||
const deltaUsage = (event as any).usage
|
const deltaUsage = event.usage
|
||||||
if (deltaUsage) {
|
if (deltaUsage) {
|
||||||
usage = updateOpenAIUsage(usage, deltaUsage)
|
usage = updateOpenAIUsage(
|
||||||
|
usage,
|
||||||
|
deltaUsage as unknown as Parameters<typeof updateOpenAIUsage>[1],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -205,8 +228,15 @@ export async function* queryModelGrok(
|
|||||||
event.type === 'message_stop' &&
|
event.type === 'message_stop' &&
|
||||||
usage.input_tokens + usage.output_tokens > 0
|
usage.input_tokens + usage.output_tokens > 0
|
||||||
) {
|
) {
|
||||||
const costUSD = calculateUSDCost(grokModel, usage as any)
|
const costUSD = calculateUSDCost(
|
||||||
addToTotalSessionCost(costUSD, usage as any, options.model)
|
grokModel,
|
||||||
|
usage as unknown as BetaUsage,
|
||||||
|
)
|
||||||
|
addToTotalSessionCost(
|
||||||
|
costUSD,
|
||||||
|
usage as unknown as BetaUsage,
|
||||||
|
options.model,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
yield {
|
yield {
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
import type {
|
||||||
|
BetaToolUnion,
|
||||||
|
BetaMessage,
|
||||||
|
BetaUsage,
|
||||||
|
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||||
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
import type { SystemPrompt } from '../../../utils/systemPromptType.js'
|
||||||
import type {
|
import type {
|
||||||
Message,
|
Message,
|
||||||
@@ -137,8 +141,8 @@ function isOpenAIConvertibleMessage(
|
|||||||
* `message_stop` handler and the post-loop safety fallback.
|
* `message_stop` handler and the post-loop safety fallback.
|
||||||
*/
|
*/
|
||||||
function assembleFinalAssistantOutputs(params: {
|
function assembleFinalAssistantOutputs(params: {
|
||||||
partialMessage: any
|
partialMessage: BetaMessage | null
|
||||||
contentBlocks: Record<number, any>
|
contentBlocks: Record<number, Record<string, unknown>>
|
||||||
tools: Tools
|
tools: Tools
|
||||||
agentId: string | undefined
|
agentId: string | undefined
|
||||||
usage: {
|
usage: {
|
||||||
@@ -166,19 +170,19 @@ function assembleFinalAssistantOutputs(params: {
|
|||||||
.map(k => contentBlocks[Number(k)])
|
.map(k => contentBlocks[Number(k)])
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|
||||||
if (allBlocks.length > 0) {
|
if (allBlocks.length > 0 && partialMessage) {
|
||||||
outputs.push({
|
outputs.push({
|
||||||
message: {
|
message: {
|
||||||
...partialMessage,
|
...partialMessage,
|
||||||
content: normalizeContentFromAPI(
|
content: normalizeContentFromAPI(
|
||||||
allBlocks,
|
allBlocks as unknown as BetaMessage['content'],
|
||||||
tools,
|
tools,
|
||||||
agentId as AgentId | undefined,
|
agentId as AgentId | undefined,
|
||||||
),
|
),
|
||||||
usage,
|
usage,
|
||||||
stop_reason: stopReason,
|
stop_reason: stopReason,
|
||||||
stop_sequence: null,
|
stop_sequence: null,
|
||||||
},
|
} as AssistantMessage['message'],
|
||||||
requestId: undefined,
|
requestId: undefined,
|
||||||
type: 'assistant',
|
type: 'assistant',
|
||||||
uuid: randomUUID(),
|
uuid: randomUUID(),
|
||||||
@@ -387,9 +391,9 @@ export async function* queryModelOpenAI(
|
|||||||
// AssistantMessage + StreamEvent (matching the Anthropic path behavior)
|
// AssistantMessage + StreamEvent (matching the Anthropic path behavior)
|
||||||
|
|
||||||
// Accumulate content blocks and usage, same as the Anthropic path in claude.ts
|
// Accumulate content blocks and usage, same as the Anthropic path in claude.ts
|
||||||
const contentBlocks: Record<number, any> = {}
|
const contentBlocks: Record<number, Record<string, unknown>> = {}
|
||||||
const collectedMessages: AssistantMessage[] = []
|
const collectedMessages: AssistantMessage[] = []
|
||||||
let partialMessage: any
|
let partialMessage: BetaMessage | null = null
|
||||||
let stopReason: string | null = null
|
let stopReason: string | null = null
|
||||||
let usage = {
|
let usage = {
|
||||||
input_tokens: 0,
|
input_tokens: 0,
|
||||||
@@ -403,19 +407,19 @@ export async function* queryModelOpenAI(
|
|||||||
for await (const event of adaptedStream) {
|
for await (const event of adaptedStream) {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case 'message_start': {
|
case 'message_start': {
|
||||||
partialMessage = (event as any).message
|
partialMessage = event.message
|
||||||
ttftMs = Date.now() - start
|
ttftMs = Date.now() - start
|
||||||
if ((event as any).message?.usage) {
|
if (event.message.usage) {
|
||||||
usage = {
|
usage = {
|
||||||
...usage,
|
...usage,
|
||||||
...(event as any).message.usage,
|
...(event.message.usage as unknown as typeof usage),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'content_block_start': {
|
case 'content_block_start': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const cb = (event as any).content_block
|
const cb = event.content_block
|
||||||
if (cb.type === 'tool_use') {
|
if (cb.type === 'tool_use') {
|
||||||
contentBlocks[idx] = { ...cb, input: '' }
|
contentBlocks[idx] = { ...cb, input: '' }
|
||||||
} else if (cb.type === 'text') {
|
} else if (cb.type === 'text') {
|
||||||
@@ -428,16 +432,18 @@ export async function* queryModelOpenAI(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'content_block_delta': {
|
case 'content_block_delta': {
|
||||||
const idx = (event as any).index
|
const idx = event.index
|
||||||
const delta = (event as any).delta
|
const delta = event.delta
|
||||||
const block = contentBlocks[idx]
|
const block = contentBlocks[idx]
|
||||||
if (!block) break
|
if (!block) break
|
||||||
if (delta.type === 'text_delta') {
|
if (delta.type === 'text_delta') {
|
||||||
block.text = (block.text || '') + delta.text
|
block.text = ((block.text as string | undefined) || '') + delta.text
|
||||||
} else if (delta.type === 'input_json_delta') {
|
} else if (delta.type === 'input_json_delta') {
|
||||||
block.input = (block.input || '') + delta.partial_json
|
block.input =
|
||||||
|
((block.input as string | undefined) || '') + delta.partial_json
|
||||||
} else if (delta.type === 'thinking_delta') {
|
} else if (delta.type === 'thinking_delta') {
|
||||||
block.thinking = (block.thinking || '') + delta.thinking
|
block.thinking =
|
||||||
|
((block.thinking as string | undefined) || '') + delta.thinking
|
||||||
} else if (delta.type === 'signature_delta') {
|
} else if (delta.type === 'signature_delta') {
|
||||||
block.signature = delta.signature
|
block.signature = delta.signature
|
||||||
}
|
}
|
||||||
@@ -448,12 +454,15 @@ export async function* queryModelOpenAI(
|
|||||||
break
|
break
|
||||||
}
|
}
|
||||||
case 'message_delta': {
|
case 'message_delta': {
|
||||||
const deltaUsage = (event as any).usage
|
const deltaUsage = event.usage
|
||||||
if (deltaUsage) {
|
if (deltaUsage) {
|
||||||
usage = updateOpenAIUsage(usage, deltaUsage)
|
usage = updateOpenAIUsage(
|
||||||
|
usage,
|
||||||
|
deltaUsage as unknown as Parameters<typeof updateOpenAIUsage>[1],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if ((event as any).delta?.stop_reason != null) {
|
if (event.delta.stop_reason != null) {
|
||||||
stopReason = (event as any).delta.stop_reason
|
stopReason = event.delta.stop_reason
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -482,8 +491,15 @@ export async function* queryModelOpenAI(
|
|||||||
}
|
}
|
||||||
// Track cost and token usage
|
// Track cost and token usage
|
||||||
if (usage.input_tokens + usage.output_tokens > 0) {
|
if (usage.input_tokens + usage.output_tokens > 0) {
|
||||||
const costUSD = calculateUSDCost(openaiModel, usage as any)
|
const costUSD = calculateUSDCost(
|
||||||
addToTotalSessionCost(costUSD, usage as any, options.model)
|
openaiModel,
|
||||||
|
usage as unknown as BetaUsage,
|
||||||
|
)
|
||||||
|
addToTotalSessionCost(
|
||||||
|
costUSD,
|
||||||
|
usage as unknown as BetaUsage,
|
||||||
|
options.model,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -117,8 +117,8 @@ export function isAnthropicAuthEnabled(): boolean {
|
|||||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
|
isEnvTruthy(process.env.CLAUDE_CODE_USE_BEDROCK) ||
|
||||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
|
isEnvTruthy(process.env.CLAUDE_CODE_USE_VERTEX) ||
|
||||||
isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
|
isEnvTruthy(process.env.CLAUDE_CODE_USE_FOUNDRY) ||
|
||||||
(settings as any).modelType === 'openai' ||
|
settings.modelType === 'openai' ||
|
||||||
(settings as any).modelType === 'gemini' ||
|
settings.modelType === 'gemini' ||
|
||||||
!!process.env.OPENAI_BASE_URL ||
|
!!process.env.OPENAI_BASE_URL ||
|
||||||
!!process.env.GEMINI_BASE_URL
|
!!process.env.GEMINI_BASE_URL
|
||||||
const apiKeyHelper = settings.apiKeyHelper
|
const apiKeyHelper = settings.apiKeyHelper
|
||||||
|
|||||||
@@ -64,12 +64,14 @@ export async function findModifiedFiles(
|
|||||||
outputsDir: string,
|
outputsDir: string,
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
// Use recursive flag to get all entries in one call
|
// Use recursive flag to get all entries in one call
|
||||||
let entries: Awaited<ReturnType<typeof fs.readdir>> | any[]
|
let entries:
|
||||||
|
| Awaited<ReturnType<typeof fs.readdir>>
|
||||||
|
| { name: string; isFile(): boolean; isSymbolicLink(): boolean }[]
|
||||||
try {
|
try {
|
||||||
entries = (await fs.readdir(outputsDir, {
|
entries = (await fs.readdir(outputsDir, {
|
||||||
withFileTypes: true,
|
withFileTypes: true,
|
||||||
recursive: true,
|
recursive: true,
|
||||||
})) as any[]
|
})) as { name: string; isFile(): boolean; isSymbolicLink(): boolean }[]
|
||||||
} catch {
|
} catch {
|
||||||
// Directory doesn't exist or is not accessible
|
// Directory doesn't exist or is not accessible
|
||||||
return []
|
return []
|
||||||
@@ -113,7 +115,7 @@ export async function findModifiedFiles(
|
|||||||
// Filter to files modified since turn start
|
// Filter to files modified since turn start
|
||||||
const modifiedFiles: string[] = []
|
const modifiedFiles: string[] = []
|
||||||
for (const result of statResults) {
|
for (const result of statResults) {
|
||||||
if (result && result.mtimeMs >= (turnStartTime as any as number)) {
|
if (result && result.mtimeMs >= turnStartTime.turnStartTime) {
|
||||||
modifiedFiles.push(result.filePath)
|
modifiedFiles.push(result.filePath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -368,7 +368,9 @@ export function isQueuedCommandEditable(cmd: QueuedCommand): boolean {
|
|||||||
export function isQueuedCommandVisible(cmd: QueuedCommand): boolean {
|
export function isQueuedCommandVisible(cmd: QueuedCommand): boolean {
|
||||||
if (
|
if (
|
||||||
(feature('KAIROS') || feature('KAIROS_CHANNELS')) &&
|
(feature('KAIROS') || feature('KAIROS_CHANNELS')) &&
|
||||||
(cmd as any).origin?.kind === 'channel'
|
(cmd as Record<string, unknown>).origin !== undefined &&
|
||||||
|
((cmd as Record<string, unknown>).origin as Record<string, unknown>)
|
||||||
|
?.kind === 'channel'
|
||||||
)
|
)
|
||||||
return true
|
return true
|
||||||
return isQueuedCommandEditable(cmd)
|
return isQueuedCommandEditable(cmd)
|
||||||
|
|||||||
@@ -137,13 +137,14 @@ const shim = {
|
|||||||
(() => {}) as typeof performance.setResourceTimingBufferSize,
|
(() => {}) as typeof performance.setResourceTimingBufferSize,
|
||||||
// Node.js v22 undici internal calls this after every fetch — must exist to
|
// Node.js v22 undici internal calls this after every fetch — must exist to
|
||||||
// avoid TypeError: markResourceTiming is not a function
|
// avoid TypeError: markResourceTiming is not a function
|
||||||
markResourceTiming: (() => {}) as any,
|
markResourceTiming: (() => {}) as () => void,
|
||||||
// Delegate read-only properties to the original
|
// Delegate read-only properties to the original
|
||||||
get timeOrigin() {
|
get timeOrigin() {
|
||||||
return original.timeOrigin
|
return original.timeOrigin
|
||||||
},
|
},
|
||||||
get onresourcetimingbufferfull() {
|
get onresourcetimingbufferfull() {
|
||||||
return (original as any).onresourcetimingbufferfull
|
return (original as unknown as typeof performance)
|
||||||
|
.onresourcetimingbufferfull
|
||||||
},
|
},
|
||||||
set onresourcetimingbufferfull(_v: any) {
|
set onresourcetimingbufferfull(_v: any) {
|
||||||
// no-op — prevent accumulation
|
// no-op — prevent accumulation
|
||||||
@@ -159,8 +160,8 @@ const shim = {
|
|||||||
* native Performance reference.
|
* native Performance reference.
|
||||||
*/
|
*/
|
||||||
export function installPerformanceShim(): void {
|
export function installPerformanceShim(): void {
|
||||||
if ((globalThis as any).__performanceShimInstalled) return
|
if ((globalThis as Record<string, unknown>).__performanceShimInstalled) return
|
||||||
;(globalThis as any).__performanceShimInstalled = true
|
;(globalThis as Record<string, unknown>).__performanceShimInstalled = true
|
||||||
globalThis.performance = shim
|
globalThis.performance = shim
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -366,19 +366,19 @@ export async function persistFileSnapshotIfRemote(): Promise<void> {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const snapshotFiles: SystemFileSnapshotMessage['snapshotFiles'] = []
|
const snapshotFiles: { key: string; path: string; content: string }[] = []
|
||||||
|
|
||||||
// Snapshot plan file
|
// Snapshot plan file
|
||||||
const plan = getPlan()
|
const plan = getPlan()
|
||||||
if (plan) {
|
if (plan) {
|
||||||
;(snapshotFiles as any[]).push({
|
snapshotFiles.push({
|
||||||
key: 'plan',
|
key: 'plan',
|
||||||
path: getPlanFilePath(),
|
path: getPlanFilePath(),
|
||||||
content: plan,
|
content: plan,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((snapshotFiles as any[]).length === 0) {
|
if (snapshotFiles.length === 0) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
type AnsiCode,
|
type AnsiCode,
|
||||||
|
type Char,
|
||||||
ansiCodesToString,
|
ansiCodesToString,
|
||||||
reduceAnsiCodes,
|
reduceAnsiCodes,
|
||||||
tokenize,
|
tokenize,
|
||||||
@@ -83,7 +84,7 @@ export default function sliceAnsi(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (include) {
|
if (include) {
|
||||||
result += (token as any).value
|
result += (token as Char).value
|
||||||
}
|
}
|
||||||
|
|
||||||
position += width
|
position += width
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
type AnsiCode,
|
type AnsiCode,
|
||||||
|
type Char,
|
||||||
ansiCodesToString,
|
ansiCodesToString,
|
||||||
reduceAnsiCodes,
|
reduceAnsiCodes,
|
||||||
type Token,
|
type Token,
|
||||||
@@ -128,14 +129,14 @@ class HighlightSegmenter {
|
|||||||
this.tokenIdx++
|
this.tokenIdx++
|
||||||
} else {
|
} else {
|
||||||
const charsNeeded = targetVisiblePos - this.visiblePos
|
const charsNeeded = targetVisiblePos - this.visiblePos
|
||||||
const charsAvailable = (token as any).value.length - this.charIdx
|
const charsAvailable = (token as Char).value.length - this.charIdx
|
||||||
const charsToTake = Math.min(charsNeeded, charsAvailable)
|
const charsToTake = Math.min(charsNeeded, charsAvailable)
|
||||||
|
|
||||||
this.stringPos += charsToTake
|
this.stringPos += charsToTake
|
||||||
this.visiblePos += charsToTake
|
this.visiblePos += charsToTake
|
||||||
this.charIdx += charsToTake
|
this.charIdx += charsToTake
|
||||||
|
|
||||||
if (this.charIdx >= (token as any).value.length) {
|
if (this.charIdx >= (token as Char).value.length) {
|
||||||
this.tokenIdx++
|
this.tokenIdx++
|
||||||
this.charIdx = 0
|
this.charIdx = 0
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user