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
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- Preview: `pnpm start`.
- Type check: `pnpm run typecheck` (or `typecheck:node` / `typecheck:web`).
- Lint/format: `pnpm run lint`, `pnpm run format`, `pnpm run format:check`.
- After completing a feature, always run `pnpm run format` and `pnpm run lint` to keep formatting and lint status clean.
- Test: `pnpm test`, `test:main`, `test:renderer`, `test:coverage`, `test:watch`, `test:ui`.
- Build: `pnpm run build` then `build:win|mac|linux` (add `:x64|:arm64`).

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,70 @@
import { LLM_PROVIDER, MODEL_META, IConfigPresenter } from '@shared/presenter'
import { LLM_PROVIDER, MODEL_META, IConfigPresenter, KeyStatus } from '@shared/presenter'
import { OpenAICompatibleProvider } from './openAICompatibleProvider'

interface CherryInUsageResponse {
total_usage: number
}

export class CherryInProvider extends OpenAICompatibleProvider {
constructor(provider: LLM_PROVIDER, configPresenter: IConfigPresenter) {
super(provider, configPresenter)
}

private getBaseUrl(): string {
return (this.provider.baseUrl || 'https://open.cherryin.ai/v1').replace(/\/$/, '')
}

public async getKeyStatus(): Promise<KeyStatus> {
if (!this.provider.apiKey) {
throw new Error('API key is required')
}

const baseUrl = this.getBaseUrl()
const headers = {
Authorization: `Bearer ${this.provider.apiKey}`,
'Content-Type': 'application/json'
}

const usageResponse = await fetch(`${baseUrl}/dashboard/billing/usage`, {
method: 'GET',
headers
})
Comment on lines +28 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add timeout to prevent indefinite hangs.

The fetch call lacks a timeout, which could cause the key status check to hang indefinitely if the CherryIn API is unresponsive.

Apply this diff to add an AbortController with timeout:

+    const abortController = new AbortController()
+    const timeoutId = setTimeout(() => abortController.abort(), 10000) // 10 second timeout
+
     const usageResponse = await fetch(`${baseUrl}/dashboard/billing/usage`, {
       method: 'GET',
-      headers
+      headers,
+      signal: abortController.signal
     })
+
+    clearTimeout(timeoutId)

Update error handling to catch AbortError:

} catch (error: unknown) {
  if (error instanceof Error && error.name === 'AbortError') {
    throw new Error('CherryIn usage check timed out')
  }
  throw error
}
🤖 Prompt for AI Agents
In src/main/presenter/llmProviderPresenter/providers/cherryInProvider.ts around
lines 28-31, the fetch call to `${baseUrl}/dashboard/billing/usage` has no
timeout and can hang; create an AbortController, pass its signal to fetch, set a
timeout (e.g. 5s or configurable) to call controller.abort(), clear the timer
after fetch completes, and update the catch block to detect an AbortError and
throw new Error('CherryIn usage check timed out') while rethrowing other errors.

Comment on lines +28 to +31
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add timeout to prevent indefinite blocking.

The fetch call lacks a timeout, which can cause the main process to hang indefinitely if the API is unresponsive. This violates the guideline about "blocking calls without timeouts on request threads."

Apply this diff to add a timeout:

-    const usageResponse = await fetch(`${baseUrl}/dashboard/billing/usage`, {
-      method: 'GET',
-      headers
-    })
+    const controller = new AbortController()
+    const timeoutId = setTimeout(() => controller.abort(), 10000) // 10 second timeout
+    
+    try {
+      const usageResponse = await fetch(`${baseUrl}/dashboard/billing/usage`, {
+        method: 'GET',
+        headers,
+        signal: controller.signal
+      })
+      clearTimeout(timeoutId)
+      
+      if (!usageResponse.ok) {
+        const errorText = await usageResponse.text()
+        throw new Error(
+          `CherryIn usage check failed: ${usageResponse.status} ${usageResponse.statusText} - ${errorText}`
+        )
+      }
+      
+      const usageData: CherryInUsageResponse = await usageResponse.json()
+      // ... rest of the logic
+    } catch (error) {
+      clearTimeout(timeoutId)
+      if (error.name === 'AbortError') {
+        throw new Error('CherryIn usage check timed out after 10 seconds')
+      }
+      throw error
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const usageResponse = await fetch(`${baseUrl}/dashboard/billing/usage`, {
method: 'GET',
headers
})
// add a timeout to prevent indefinite blocking
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 10000) // 10 second timeout
try {
const usageResponse = await fetch(`${baseUrl}/dashboard/billing/usage`, {
method: 'GET',
headers,
signal: controller.signal
})
clearTimeout(timeoutId)
if (!usageResponse.ok) {
const errorText = await usageResponse.text()
throw new Error(
`CherryIn usage check failed: ${usageResponse.status} ${usageResponse.statusText} - ${errorText}`
)
}
const usageData: CherryInUsageResponse = await usageResponse.json()
// ... rest of the existing logic to extract total_usage, convert to USD, and return
} catch (error: any) {
clearTimeout(timeoutId)
if (error.name === 'AbortError') {
throw new Error('CherryIn usage check timed out after 10 seconds')
}
throw error
}
🤖 Prompt for AI Agents
In src/main/presenter/llmProviderPresenter/providers/cherryInProvider.ts around
lines 28 to 31, the fetch to `${baseUrl}/dashboard/billing/usage` has no timeout
and can hang; wrap the fetch in an AbortController, start a timer (e.g.,
setTimeout) that calls controller.abort() after a reasonable timeout (e.g.,
5–10s), pass controller.signal to fetch, clear the timer on success, and handle
the abort error path to return a controlled timeout error/response instead of
leaving the request hanging.


if (!usageResponse.ok) {
const errorText = await usageResponse.text()
throw new Error(
`CherryIn usage check failed: ${usageResponse.status} ${usageResponse.statusText} - ${errorText}`
)
}

const usageData: CherryInUsageResponse = await usageResponse.json()

const totalUsage = Number(usageData?.total_usage)

const usageUsd = Number.isFinite(totalUsage) ? totalUsage / 100 : 0

return {
usage: `$${usageUsd.toFixed(2)}`
}
}

public async check(): Promise<{ isOk: boolean; errorMsg: string | null }> {
try {
await this.getKeyStatus()
return { isOk: true, errorMsg: null }
} catch (error: unknown) {
let errorMessage = 'An unknown error occurred during CherryIn API key check.'
if (error instanceof Error) {
errorMessage = error.message
} else if (typeof error === 'string') {
errorMessage = error
}

console.error('CherryIn API key check failed:', error)
return { isOk: false, errorMsg: errorMessage }
}
}

protected async fetchOpenAIModels(options?: { timeout: number }): Promise<MODEL_META[]> {
try {
const models = await super.fetchOpenAIModels(options)
Expand Down
2 changes: 1 addition & 1 deletion src/renderer/settings/components/ProviderApiConfig.vue
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ const openModelCheckDialog = () => {

const getKeyStatus = async () => {
if (
['ppio', 'openrouter', 'siliconcloud', 'silicon', 'deepseek', '302ai'].includes(
['ppio', 'openrouter', 'siliconcloud', 'silicon', 'deepseek', '302ai', 'cherryin'].includes(
props.provider.id
) &&
props.provider.apiKey
Expand Down