{"record":{"id":"7e296c7e2d797935","repo":"CherryHQ/cherry-studio","slug":"modelscope-api-error-response-status-error","errorCode":null,"errorMessage":"ModelScope API error: ${response.status} - ${errorText}","messagePattern":"ModelScope API error: (.+?) - (.+?)","errorType":"exception","errorClass":"ModelscopeApiError","httpStatus":null,"severity":"error","filePath":"src/main/ai/provider/custom/modelscope/modelscopeTransport.ts","lineNumber":230,"sourceCode":"\n    const fetchOptions: RequestInit = {\n      method,\n      headers: {\n        Authorization: `Bearer ${this.apiKey}`,\n        ...(method === 'POST' && { 'Content-Type': 'application/json' }),\n        ...options.extraHeaders\n      },\n      signal: controller.signal\n    }\n    if (method === 'POST' && body !== undefined) {\n      fetchOptions.body = JSON.stringify(body)\n    }\n\n    try {\n      const response = await fetch(`${this.baseURL}${path}`, fetchOptions)\n      if (!response.ok) {\n        const errorText = (await response.text().catch(() => '')).slice(0, 500)\n        throw new ModelscopeApiError(`ModelScope API error: ${response.status} - ${errorText}`, response.status)\n      }\n      return (await response.json()) as T\n    } catch (error) {\n      if (error instanceof Error && error.name === 'AbortError') {\n        if (externallyAborted) throw createAbortError('ModelScope API request aborted')\n        throw new Error(`ModelScope API request timeout after ${timeout / 1000}s`)\n      }\n      throw error\n    } finally {\n      clearTimeout(timeoutId)\n      externalSignal?.removeEventListener('abort', onExternalAbort)\n    }\n  }\n}\n\nexport function createModelscopeTransport(settings: ModelscopeTransportSettings): ModelscopeTransport {\n  return new ModelscopeTransport(settings)\n}","sourceCodeStart":212,"sourceCodeEnd":248,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/provider/custom/modelscope/modelscopeTransport.ts#L212-L248","documentation":"ModelscopeApiError is thrown by ModelscopeTransport.request() whenever the ModelScope (api-inference.modelscope.cn) HTTP response has a non-ok status. It is a custom Error subclass that carries the originating HTTP status code on `error.statusCode` so callers (e.g. the poll loop) can classify terminal vs transient failures via isTerminalHttpStatus. The message embeds both the status and the first 500 chars of the response body for diagnostics.","triggerScenarios":"POST /v1/images/generations or GET /v1/tasks/{id} returns 4xx/5xx. Concrete causes: missing/wrong API key (401/403), exhausted free-tier quota (429), unknown model id in the body (400/404), malformed request body (422), or a 5xx during vendor outage. The submit call uses X-ModelScope-Async-Mode:true; omitting or mis-encoding size/steps/guidance triggers 400.","commonSituations":"First-run with an unset/typo'd apiKey, wrong baseURL override, model id drift after ModelScope renames a model, size string not in WxH format, guidance/steps sent under wrong camelCase spelling, or rate-limit hits during batch generation.","solutions":["Read error.statusCode and error.message: if 401/403, fix the API key in provider settings; if 429, back off and respect quota; if 400/422, inspect the embedded errorText for the offending field.","Verify the model id is still listed in ModelScope's api-inference catalog and matches what the registry sends.","If status >= 500 or 429, treat as transient and retry (the poll loop already does this for up to maxTransientRetries; for submit, add an outer retry).","Confirm baseURL is the api-inference host (default https://api-inference.modelscope.cn) and not the model repos host."],"exampleFix":"// before\nawait this.request('/v1/images/generations', 'POST', body, { timeout: 120000 })\n// after — classify terminal vs transient at the call site\ntry {\n  await this.request('/v1/images/generations', 'POST', body, { timeout: 120000 })\n} catch (e) {\n  if (e instanceof ModelscopeApiError && isTerminalHttpStatus(e.statusCode)) throw e\n  // transient: surface to user with retry option\n}","handlingStrategy":"try-catch","validationCode":"// Validate key + baseURL before submit\nif (!settings.apiKey) throw new Error('ModelScope apiKey is required')\nif (!/^https?:\\/\\//.test(baseURL)) throw new Error(`Invalid ModelScope baseURL: ${baseURL}`)","typeGuard":"export function isModelscopeApiError(e: unknown): e is { statusCode: number; message: string } {\n  return e instanceof Error && e.name === 'ModelscopeApiError' && typeof (e as any).statusCode === 'number'\n}","tryCatchPattern":"try {\n  await transport.submit(input)\n} catch (e) {\n  if (isModelscopeApiError(e) && isTerminalHttpStatus(e.statusCode)) {\n    // 4xx (not 429): surface, do not retry\n    throw e\n  }\n  // 5xx / 429: retry with backoff\n  await backoffRetry(() => transport.submit(input))\n}","preventionTips":["Always set and validate the ModelScope apiKey before constructing the transport.","Use the default api-inference.modelscope.cn baseURL unless you have a known override.","Catch ModelscopeApiError and key off statusCode to separate terminal from transient failures."],"tags":["network","api","modelscope","http","image-generation"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}