Compare commits

...

6 Commits

Author SHA1 Message Date
nighttrek 2e11e07411 refactor(ts): improve type safety for request ID capture and empty-assistant diagnostics
cline.ts: strongly type custom fetch override using Parameters<typeof fetch>/ReturnType<typeof fetch>; safe URL extraction for string|URL|Request; removed any casts.

task/index.ts: encapsulate requestId retrieval via getApiRequestIdSafe(); avoid any; no behavior change. Scope limited to PR 6066-related changes.
2025-09-08 15:46:09 -07:00
Daniel Steigman 32bdc3884e Merge branch 'main' into nighttrek/api-error-tracking 2025-09-08 12:07:30 -07:00
Daniel Steigman c389507c7b Delete PR_BODY_nighttrek_api_error_tracking.md 2025-09-08 10:54:22 -07:00
Daniel Steigman d291c11850 Merge branch 'main' into nighttrek/api-error-tracking 2025-09-08 01:36:29 -07:00
NightTrek 57fa7678c1 feat(cline): capture real HTTP request ID (X-Request-ID) in ClineHandler via custom fetch; expose getLastRequestId(); prefer true requestId over generationId in Task telemetry for empty-assistant-message 2025-09-08 01:30:04 -07:00
NightTrek edf18b318d added telemetry around uexpected API responses 2025-09-07 22:38:20 -07:00
3 changed files with 65 additions and 4 deletions
+33
View File
@@ -32,6 +32,7 @@ export class ClineHandler implements ApiHandler {
private client: OpenAI | undefined
private readonly _baseUrl = clineEnvConfig.apiBaseUrl
lastGenerationId?: string
private lastRequestId?: string
constructor(options: ClineHandlerOptions) {
this.options = options
@@ -54,6 +55,31 @@ export class ClineHandler implements ApiHandler {
"X-Task-ID": this.options.ulid || "",
"X-Cline-Version": extensionVersion,
},
// Capture real HTTP request ID from initial streaming response headers
fetch: async (...args: Parameters<typeof fetch>): Promise<Awaited<ReturnType<typeof fetch>>> => {
const [input, init] = args
const resp = await fetch(input, init)
try {
let urlStr = ""
if (typeof input === "string") {
urlStr = input
} else if (input instanceof URL) {
urlStr = input.toString()
} else if (typeof (input as { url?: unknown }).url === "string") {
urlStr = (input as { url: string }).url
}
// Only record for chat completions (the primary streaming request)
if (urlStr.includes("/chat/completions")) {
const rid = resp.headers.get("x-request-id") || resp.headers.get("request-id")
if (rid) {
this.lastRequestId = rid
}
}
} catch {
// ignore header capture errors
}
return resp
},
})
} catch (error: any) {
throw new Error(`Error creating Cline client: ${error.message}`)
@@ -70,6 +96,7 @@ export class ClineHandler implements ApiHandler {
const client = await this.ensureClient()
this.lastGenerationId = undefined
this.lastRequestId = undefined
let didOutputUsage: boolean = false
@@ -92,6 +119,7 @@ export class ClineHandler implements ApiHandler {
const metadataStr = error.metadata ? `\nMetadata: ${JSON.stringify(error.metadata, null, 2)}` : ""
throw new Error(`Cline API Error ${error.code}: ${error.message}${metadataStr}`)
}
if (!this.lastGenerationId && chunk.id) {
this.lastGenerationId = chunk.id
}
@@ -203,6 +231,11 @@ export class ClineHandler implements ApiHandler {
return undefined
}
// Expose the last HTTP request ID captured from response headers (X-Request-ID)
getLastRequestId(): string | undefined {
return this.lastRequestId
}
getModel(): { id: string; info: ModelInfo } {
const modelId = this.options.openRouterModelId
const modelInfo = this.options.openRouterModelInfo
+31 -4
View File
@@ -1273,6 +1273,14 @@ export class Task {
return { model, providerId, customPrompt }
}
private getApiRequestIdSafe(): string | undefined {
const apiLike = this.api as Partial<{
getLastRequestId: () => string | undefined
lastGenerationId?: string
}>
return apiLike.getLastRequestId?.() ?? apiLike.lastGenerationId
}
private async handleContextWindowExceededError(): Promise<void> {
const apiConversationHistory = this.messageStateHandler.getApiConversationHistory()
@@ -2147,10 +2155,29 @@ export class Task {
didEndLoop = recDidEndLoop
} else {
// if there's no assistant_responses, that means we got no text or tool_use content blocks from API which we should assume is an error
await this.say(
"error",
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output.",
)
const { model, providerId } = this.getCurrentProviderInfo()
const reqId = this.getApiRequestIdSafe()
// Minimal diagnostics: structured log and telemetry
console.error("[EmptyAssistantMessage]", {
ulid: this.ulid,
providerId,
modelId: model.id,
requestId: reqId,
})
telemetryService.captureProviderApiError({
ulid: this.ulid,
model: model.id,
provider: providerId,
errorMessage: "empty_assistant_message",
requestId: reqId,
})
const baseErrorMessage =
"Unexpected API Response: The language model did not provide any assistant messages. This may indicate an issue with the API or the model's output."
const errorText = reqId ? `${baseErrorMessage} (reqId: ${reqId})` : baseErrorMessage
await this.say("error", errorText)
await this.messageStateHandler.addToApiConversationHistory({
role: "assistant",
content: [
@@ -730,6 +730,7 @@ export class TelemetryService {
ulid: string
model: string
errorMessage: string
provider?: string
errorStatus?: number | undefined
requestId?: string | undefined
}) {