Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions packages/cli/src/ui/hooks/useGeminiStream.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,11 @@ const MockedGeminiClientClass = vi.hoisted(() =>
const MockedUserPromptEvent = vi.hoisted(() =>
vi.fn().mockImplementation(() => {}),
);
const MockedApiCancelEvent = vi.hoisted(() =>
vi.fn().mockImplementation(() => {}),
);
const mockParseAndFormatApiError = vi.hoisted(() => vi.fn());
const mockLogApiCancel = vi.hoisted(() => vi.fn());

// Vision auto-switch mocks (hoisted)
const mockHandleVisionSwitch = vi.hoisted(() =>
Expand All @@ -71,7 +75,9 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
GitService: vi.fn(),
GeminiClient: MockedGeminiClientClass,
UserPromptEvent: MockedUserPromptEvent,
ApiCancelEvent: MockedApiCancelEvent,
parseAndFormatApiError: mockParseAndFormatApiError,
logApiCancel: mockLogApiCancel,
};
});

Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/ui/hooks/useGeminiStream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import {
ConversationFinishedEvent,
ApprovalMode,
parseAndFormatApiError,
logApiCancel,
ApiCancelEvent,
} from '@qwen-code/qwen-code-core';
import { type Part, type PartListUnion, FinishReason } from '@google/genai';
import type {
Expand Down Expand Up @@ -223,6 +225,16 @@ export const useGeminiStream = (
turnCancelledRef.current = true;
isSubmittingQueryRef.current = false;
abortControllerRef.current?.abort();

// Log API cancellation
const prompt_id = config.getSessionId() + '########' + getPromptCount();
const cancellationEvent = new ApiCancelEvent(
config.getModel(),
prompt_id,
config.getContentGeneratorConfig()?.authType,
);
logApiCancel(config, cancellationEvent);

if (pendingHistoryItemRef.current) {
addItem(pendingHistoryItemRef.current, Date.now());
}
Expand All @@ -242,6 +254,8 @@ export const useGeminiStream = (
setPendingHistoryItem,
onCancelSubmit,
pendingHistoryItemRef,
config,
getPromptCount,
]);

useKeypress(
Expand Down Expand Up @@ -448,6 +462,7 @@ export const useGeminiStream = (
if (turnCancelledRef.current) {
return;
}

if (pendingHistoryItemRef.current) {
if (pendingHistoryItemRef.current.type === 'tool_group') {
const updatedTools = pendingHistoryItemRef.current.tools.map(
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/zed-integration/zedIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@ class Session {
function_name: fc.name ?? '',
function_args: args,
duration_ms: durationMs,
status: 'error',
success: false,
error: error.message,
tool_type:
Expand Down Expand Up @@ -483,6 +484,7 @@ class Session {
function_name: fc.name,
function_args: args,
duration_ms: durationMs,
status: 'success',
success: true,
prompt_id: promptId,
tool_type:
Expand Down
4 changes: 3 additions & 1 deletion packages/core/src/core/coreToolScheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ export class CoreToolScheduler {
}
}

return {
const cancelledCall = {
request: currentCall.request,
tool: toolInstance,
invocation,
Expand All @@ -426,6 +426,8 @@ export class CoreToolScheduler {
durationMs,
outcome,
} as CancelledToolCall;

return cancelledCall;
}
case 'validating':
return {
Expand Down
6 changes: 6 additions & 0 deletions packages/core/src/core/openaiContentGenerator/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,12 @@ export class ContentGenerationPipeline {
mergedResponse.usageMetadata = lastResponse.usageMetadata;
}

// Copy other essential properties from the current response
mergedResponse.responseId = response.responseId;
mergedResponse.createTime = response.createTime;
mergedResponse.modelVersion = response.modelVersion;
mergedResponse.promptFeedback = response.promptFeedback;

// Update the collected responses with the merged response
collectedGeminiResponses[collectedGeminiResponses.length - 1] =
mergedResponse;
Expand Down
8 changes: 8 additions & 0 deletions packages/core/src/core/turn.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ export interface ToolCallRequestInfo {
args: Record<string, unknown>;
isClientInitiated: boolean;
prompt_id: string;
response_id?: string;
}

export interface ToolCallResponseInfo {
Expand Down Expand Up @@ -202,6 +203,7 @@ export class Turn {
readonly pendingToolCalls: ToolCallRequestInfo[];
private debugResponses: GenerateContentResponse[];
finishReason: FinishReason | undefined;
private currentResponseId?: string;

constructor(
private readonly chat: GeminiChat,
Expand Down Expand Up @@ -247,6 +249,11 @@ export class Turn {

this.debugResponses.push(resp);

// Track the current response ID for tool call correlation
if (resp.responseId) {
this.currentResponseId = resp.responseId;
}

const thoughtPart = resp.candidates?.[0]?.content?.parts?.[0];
if (thoughtPart?.thought) {
// Thought always has a bold "subject" part enclosed in double asterisks
Expand Down Expand Up @@ -346,6 +353,7 @@ export class Turn {
args,
isClientInitiated: false,
prompt_id: this.prompt_id,
response_id: this.currentResponseId,
};

this.pendingToolCalls.push(toolCallRequest);
Expand Down
9 changes: 9 additions & 0 deletions packages/core/src/subagents/subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,7 @@ export class SubAgentScope {
let roundText = '';
let lastUsage: GenerateContentResponseUsageMetadata | undefined =
undefined;
let currentResponseId: string | undefined = undefined;
for await (const streamEvent of responseStream) {
if (abortController.signal.aborted) {
this.terminateMode = SubagentTerminateMode.CANCELLED;
Expand All @@ -395,6 +396,10 @@ export class SubAgentScope {
// Handle chunk events
if (streamEvent.type === 'chunk') {
const resp = streamEvent.value;
// Track the response ID for tool call correlation
if (resp.responseId) {
currentResponseId = resp.responseId;
}
if (resp.functionCalls) functionCalls.push(...resp.functionCalls);
const content = resp.candidates?.[0]?.content;
const parts = content?.parts || [];
Expand Down Expand Up @@ -455,6 +460,7 @@ export class SubAgentScope {
abortController,
promptId,
turnCounter,
currentResponseId,
);
} else {
// No tool calls β€” treat this as the model's final answer.
Expand Down Expand Up @@ -543,6 +549,7 @@ export class SubAgentScope {
* @param {FunctionCall[]} functionCalls - An array of `FunctionCall` objects to process.
* @param {ToolRegistry} toolRegistry - The tool registry to look up and execute tools.
* @param {AbortController} abortController - An `AbortController` to signal cancellation of tool executions.
* @param {string} responseId - Optional API response ID for correlation with tool calls.
* @returns {Promise<Content[]>} A promise that resolves to an array of `Content` parts representing the tool responses,
* which are then used to update the chat history.
*/
Expand All @@ -551,6 +558,7 @@ export class SubAgentScope {
abortController: AbortController,
promptId: string,
currentRound: number,
responseId?: string,
): Promise<Content[]> {
const toolResponseParts: Part[] = [];

Expand Down Expand Up @@ -704,6 +712,7 @@ export class SubAgentScope {
args,
isClientInitiated: true,
prompt_id: promptId,
response_id: responseId,
};

const description = this.getToolDescription(toolName, args);
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/telemetry/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const EVENT_USER_PROMPT = 'qwen-code.user_prompt';
export const EVENT_TOOL_CALL = 'qwen-code.tool_call';
export const EVENT_API_REQUEST = 'qwen-code.api_request';
export const EVENT_API_ERROR = 'qwen-code.api_error';
export const EVENT_API_CANCEL = 'qwen-code.api_cancel';
export const EVENT_API_RESPONSE = 'qwen-code.api_response';
export const EVENT_CLI_CONFIG = 'qwen-code.config';
export const EVENT_FLASH_FALLBACK = 'qwen-code.flash_fallback';
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/telemetry/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export { SpanStatusCode, ValueType } from '@opentelemetry/api';
export { SemanticAttributes } from '@opentelemetry/semantic-conventions';
export {
logApiError,
logApiCancel,
logApiRequest,
logApiResponse,
logChatCompression,
Expand All @@ -35,6 +36,7 @@ export {
} from './sdk.js';
export {
ApiErrorEvent,
ApiCancelEvent,
ApiRequestEvent,
ApiResponseEvent,
ConversationFinishedEvent,
Expand All @@ -54,4 +56,5 @@ export type {
TelemetryEvent,
} from './types.js';
export * from './uiTelemetry.js';
export { QwenLogger } from './qwen-logger/qwen-logger.js';
export { DEFAULT_OTLP_ENDPOINT, DEFAULT_TELEMETRY_TARGET };
5 changes: 5 additions & 0 deletions packages/core/src/telemetry/loggers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,7 @@ describe('loggers', () => {
2,
),
duration_ms: 100,
status: 'success',
success: true,
decision: ToolCallDecision.ACCEPT,
prompt_id: 'prompt-id-1',
Expand Down Expand Up @@ -619,6 +620,7 @@ describe('loggers', () => {
2,
),
duration_ms: 100,
status: 'error',
success: false,
decision: ToolCallDecision.REJECT,
prompt_id: 'prompt-id-2',
Expand Down Expand Up @@ -691,6 +693,7 @@ describe('loggers', () => {
2,
),
duration_ms: 100,
status: 'success',
success: true,
decision: ToolCallDecision.MODIFY,
prompt_id: 'prompt-id-3',
Expand Down Expand Up @@ -762,6 +765,7 @@ describe('loggers', () => {
2,
),
duration_ms: 100,
status: 'success',
success: true,
prompt_id: 'prompt-id-4',
tool_type: 'native',
Expand Down Expand Up @@ -834,6 +838,7 @@ describe('loggers', () => {
2,
),
duration_ms: 100,
status: 'error',
success: false,
error: 'test-error',
'error.message': 'test-error',
Expand Down
28 changes: 28 additions & 0 deletions packages/core/src/telemetry/loggers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { safeJsonStringify } from '../utils/safeJsonStringify.js';
import { UserAccountManager } from '../utils/userAccountManager.js';
import {
EVENT_API_ERROR,
EVENT_API_CANCEL,
EVENT_API_REQUEST,
EVENT_API_RESPONSE,
EVENT_CHAT_COMPRESSION,
Expand Down Expand Up @@ -45,6 +46,7 @@ import { QwenLogger } from './qwen-logger/qwen-logger.js';
import { isTelemetrySdkInitialized } from './sdk.js';
import type {
ApiErrorEvent,
ApiCancelEvent,
ApiRequestEvent,
ApiResponseEvent,
ChatCompressionEvent,
Expand Down Expand Up @@ -282,6 +284,32 @@ export function logApiError(config: Config, event: ApiErrorEvent): void {
);
}

export function logApiCancel(config: Config, event: ApiCancelEvent): void {
const uiEvent = {
...event,
'event.name': EVENT_API_CANCEL,
'event.timestamp': new Date().toISOString(),
} as UiEvent;
uiTelemetryService.addEvent(uiEvent);
QwenLogger.getInstance(config)?.logApiCancelEvent(event);
if (!isTelemetrySdkInitialized()) return;

const attributes: LogAttributes = {
...getCommonAttributes(config),
...event,
'event.name': EVENT_API_CANCEL,
'event.timestamp': new Date().toISOString(),
model_name: event.model,
};

const logger = logs.getLogger(SERVICE_NAME);
const logRecord: LogRecord = {
body: `API request cancelled for ${event.model}.`,
attributes,
};
logger.emit(logRecord);
}

export function logApiResponse(config: Config, event: ApiResponseEvent): void {
const uiEvent = {
...event,
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/telemetry/qwen-logger/qwen-logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {
ApiRequestEvent,
ApiResponseEvent,
ApiErrorEvent,
ApiCancelEvent,
FileOperationEvent,
FlashFallbackEvent,
LoopDetectedEvent,
Expand Down Expand Up @@ -411,6 +412,7 @@ export class QwenLogger {
{
properties: {
prompt_id: event.prompt_id,
response_id: event.response_id,
},
snapshots: JSON.stringify({
function_name: event.function_name,
Expand All @@ -427,6 +429,19 @@ export class QwenLogger {
this.flushIfNeeded();
}

logApiCancelEvent(event: ApiCancelEvent): void {
const rumEvent = this.createActionEvent('api', 'api_cancel', {
properties: {
model: event.model,
prompt_id: event.prompt_id,
auth_type: event.auth_type,
},
});

this.enqueueLogEvent(rumEvent);
this.flushIfNeeded();
}

logFileOperationEvent(event: FileOperationEvent): void {
const rumEvent = this.createActionEvent(
'file_operation',
Expand Down
Loading