@evolinkai/mcp 0.0.0-stage → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. package/LICENSE +178 -0
  2. package/README.md +67 -2
  3. package/dist/core/src/branding.d.ts +3 -0
  4. package/dist/core/src/branding.d.ts.map +1 -0
  5. package/dist/core/src/branding.js +8 -0
  6. package/dist/core/src/branding.js.map +1 -0
  7. package/dist/core/src/config.d.ts +19 -0
  8. package/dist/core/src/config.d.ts.map +1 -0
  9. package/dist/core/src/config.js +90 -0
  10. package/dist/core/src/config.js.map +1 -0
  11. package/dist/core/src/data/model-params.d.ts +49 -0
  12. package/dist/core/src/data/model-params.d.ts.map +1 -0
  13. package/dist/core/src/data/model-params.generated.d.ts +2 -0
  14. package/dist/core/src/data/model-params.generated.d.ts.map +1 -0
  15. package/dist/core/src/data/model-params.generated.js +3 -0
  16. package/dist/core/src/data/model-params.generated.js.map +1 -0
  17. package/dist/core/src/data/model-params.js +22 -0
  18. package/dist/core/src/data/model-params.js.map +1 -0
  19. package/dist/core/src/request-context.d.ts +79 -0
  20. package/dist/core/src/request-context.d.ts.map +1 -0
  21. package/dist/core/src/request-context.js +52 -0
  22. package/dist/core/src/request-context.js.map +1 -0
  23. package/dist/core/src/server.d.ts +36 -0
  24. package/dist/core/src/server.d.ts.map +1 -0
  25. package/dist/core/src/server.js +123 -0
  26. package/dist/core/src/server.js.map +1 -0
  27. package/dist/core/src/services/api-client.d.ts +142 -0
  28. package/dist/core/src/services/api-client.d.ts.map +1 -0
  29. package/dist/core/src/services/api-client.js +179 -0
  30. package/dist/core/src/services/api-client.js.map +1 -0
  31. package/dist/core/src/services/error-handler.d.ts +51 -0
  32. package/dist/core/src/services/error-handler.d.ts.map +1 -0
  33. package/dist/core/src/services/error-handler.js +403 -0
  34. package/dist/core/src/services/error-handler.js.map +1 -0
  35. package/dist/core/src/services/file-client.d.ts +54 -0
  36. package/dist/core/src/services/file-client.d.ts.map +1 -0
  37. package/dist/core/src/services/file-client.js +131 -0
  38. package/dist/core/src/services/file-client.js.map +1 -0
  39. package/dist/core/src/services/http-policy.d.ts +40 -0
  40. package/dist/core/src/services/http-policy.d.ts.map +1 -0
  41. package/dist/core/src/services/http-policy.js +120 -0
  42. package/dist/core/src/services/http-policy.js.map +1 -0
  43. package/dist/core/src/services/media-preview.d.ts +24 -0
  44. package/dist/core/src/services/media-preview.d.ts.map +1 -0
  45. package/dist/core/src/services/media-preview.js +155 -0
  46. package/dist/core/src/services/media-preview.js.map +1 -0
  47. package/dist/core/src/services/model-catalog.d.ts +24 -0
  48. package/dist/core/src/services/model-catalog.d.ts.map +1 -0
  49. package/dist/core/src/services/model-catalog.js +45 -0
  50. package/dist/core/src/services/model-catalog.js.map +1 -0
  51. package/dist/core/src/services/param-validator.d.ts +21 -0
  52. package/dist/core/src/services/param-validator.d.ts.map +1 -0
  53. package/dist/core/src/services/param-validator.js +164 -0
  54. package/dist/core/src/services/param-validator.js.map +1 -0
  55. package/dist/core/src/services/preview-download.d.ts +11 -0
  56. package/dist/core/src/services/preview-download.d.ts.map +1 -0
  57. package/dist/core/src/services/preview-download.js +106 -0
  58. package/dist/core/src/services/preview-download.js.map +1 -0
  59. package/dist/core/src/services/pricing-client.d.ts +95 -0
  60. package/dist/core/src/services/pricing-client.d.ts.map +1 -0
  61. package/dist/core/src/services/pricing-client.js +396 -0
  62. package/dist/core/src/services/pricing-client.js.map +1 -0
  63. package/dist/core/src/services/upload-policy.d.ts +19 -0
  64. package/dist/core/src/services/upload-policy.d.ts.map +1 -0
  65. package/dist/core/src/services/upload-policy.js +168 -0
  66. package/dist/core/src/services/upload-policy.js.map +1 -0
  67. package/dist/core/src/services/upload-slots.d.ts +63 -0
  68. package/dist/core/src/services/upload-slots.d.ts.map +1 -0
  69. package/dist/core/src/services/upload-slots.js +24 -0
  70. package/dist/core/src/services/upload-slots.js.map +1 -0
  71. package/dist/core/src/services/utm.d.ts +29 -0
  72. package/dist/core/src/services/utm.d.ts.map +1 -0
  73. package/dist/core/src/services/utm.js +64 -0
  74. package/dist/core/src/services/utm.js.map +1 -0
  75. package/dist/core/src/tool-guidance.d.ts +6 -0
  76. package/dist/core/src/tool-guidance.d.ts.map +1 -0
  77. package/dist/core/src/tool-guidance.js +49 -0
  78. package/dist/core/src/tool-guidance.js.map +1 -0
  79. package/dist/core/src/tools/check-balance.d.ts +4 -0
  80. package/dist/core/src/tools/check-balance.d.ts.map +1 -0
  81. package/dist/core/src/tools/check-balance.js +82 -0
  82. package/dist/core/src/tools/check-balance.js.map +1 -0
  83. package/dist/core/src/tools/estimate-cost.d.ts +4 -0
  84. package/dist/core/src/tools/estimate-cost.d.ts.map +1 -0
  85. package/dist/core/src/tools/estimate-cost.js +131 -0
  86. package/dist/core/src/tools/estimate-cost.js.map +1 -0
  87. package/dist/core/src/tools/generate.d.ts +4 -0
  88. package/dist/core/src/tools/generate.d.ts.map +1 -0
  89. package/dist/core/src/tools/generate.js +221 -0
  90. package/dist/core/src/tools/generate.js.map +1 -0
  91. package/dist/core/src/tools/get-model.d.ts +5 -0
  92. package/dist/core/src/tools/get-model.d.ts.map +1 -0
  93. package/dist/core/src/tools/get-model.js +117 -0
  94. package/dist/core/src/tools/get-model.js.map +1 -0
  95. package/dist/core/src/tools/get-task.d.ts +7 -0
  96. package/dist/core/src/tools/get-task.d.ts.map +1 -0
  97. package/dist/core/src/tools/get-task.js +56 -0
  98. package/dist/core/src/tools/get-task.js.map +1 -0
  99. package/dist/core/src/tools/list-tasks.d.ts +6 -0
  100. package/dist/core/src/tools/list-tasks.d.ts.map +1 -0
  101. package/dist/core/src/tools/list-tasks.js +149 -0
  102. package/dist/core/src/tools/list-tasks.js.map +1 -0
  103. package/dist/core/src/tools/prepare-upload.d.ts +6 -0
  104. package/dist/core/src/tools/prepare-upload.d.ts.map +1 -0
  105. package/dist/core/src/tools/prepare-upload.js +134 -0
  106. package/dist/core/src/tools/prepare-upload.js.map +1 -0
  107. package/dist/core/src/tools/search-models.d.ts +5 -0
  108. package/dist/core/src/tools/search-models.d.ts.map +1 -0
  109. package/dist/core/src/tools/search-models.js +97 -0
  110. package/dist/core/src/tools/search-models.js.map +1 -0
  111. package/dist/core/src/tools/shared.d.ts +55 -0
  112. package/dist/core/src/tools/shared.d.ts.map +1 -0
  113. package/dist/core/src/tools/shared.js +137 -0
  114. package/dist/core/src/tools/shared.js.map +1 -0
  115. package/dist/core/src/tools/task-format.d.ts +48 -0
  116. package/dist/core/src/tools/task-format.d.ts.map +1 -0
  117. package/dist/core/src/tools/task-format.js +212 -0
  118. package/dist/core/src/tools/task-format.js.map +1 -0
  119. package/dist/core/src/tools/upload-file.d.ts +17 -0
  120. package/dist/core/src/tools/upload-file.d.ts.map +1 -0
  121. package/dist/core/src/tools/upload-file.js +134 -0
  122. package/dist/core/src/tools/upload-file.js.map +1 -0
  123. package/dist/core/src/version.d.ts +3 -0
  124. package/dist/core/src/version.d.ts.map +1 -0
  125. package/dist/core/src/version.js +3 -0
  126. package/dist/core/src/version.js.map +1 -0
  127. package/dist/evolink-media/src/index.d.ts +3 -0
  128. package/dist/evolink-media/src/index.d.ts.map +1 -0
  129. package/dist/evolink-media/src/index.js +18 -0
  130. package/dist/evolink-media/src/index.js.map +1 -0
  131. package/package.json +46 -4
@@ -0,0 +1,179 @@
1
+ import { gatewayAuthHeaders } from '../config.js';
2
+ import { classifyGatewayError, formatGatewayError } from './error-handler.js';
3
+ import { DEFAULT_READ_TIMEOUT_MS, DEFAULT_SUBMIT_TIMEOUT_MS, MAX_RETRY_DELAY_MS, PaidRequestOutcomeUnknownError, RequestTimeoutError, evoHeaders, fetchWithTimeout, newRunId, parseRetryAfter, readJsonBody, responseRequestId, timeoutFromEnv, } from './http-policy.js';
4
+ // --- Error class for HTTP-level failures ---
5
+ export class ApiHttpError extends Error {
6
+ status;
7
+ retryAfterMs;
8
+ requestId;
9
+ info;
10
+ constructor(status, message, retryAfterMs, requestId, info) {
11
+ super(message);
12
+ this.status = status;
13
+ this.retryAfterMs = retryAfterMs;
14
+ this.requestId = requestId;
15
+ this.info = info;
16
+ this.name = 'ApiHttpError';
17
+ }
18
+ }
19
+ // --- Retry logic ---
20
+ const RETRYABLE_STATUS_CODES = new Set([429, 502, 503]);
21
+ function sleep(ms) {
22
+ return new Promise(resolve => setTimeout(resolve, ms));
23
+ }
24
+ function isRetryable(error) {
25
+ // Key and balance rejections are always HTTP 402, so they are never retried here.
26
+ if (error instanceof ApiHttpError)
27
+ return RETRYABLE_STATUS_CODES.has(error.status);
28
+ if (error instanceof PaidRequestOutcomeUnknownError)
29
+ return true;
30
+ if (error instanceof RequestTimeoutError)
31
+ return true;
32
+ if (error instanceof TypeError)
33
+ return true; // network errors (DNS, reset, …)
34
+ return false;
35
+ }
36
+ export async function withRetry(fn, retries, baseDelayMs) {
37
+ let lastError;
38
+ for (let attempt = 0; attempt <= retries; attempt++) {
39
+ try {
40
+ return await fn();
41
+ }
42
+ catch (error) {
43
+ lastError = error;
44
+ if (attempt < retries && isRetryable(error)) {
45
+ const retryAfterMs = error instanceof ApiHttpError ? error.retryAfterMs : undefined;
46
+ await sleep(Math.min(retryAfterMs ?? baseDelayMs * (attempt + 1), MAX_RETRY_DELAY_MS));
47
+ continue;
48
+ }
49
+ throw error;
50
+ }
51
+ }
52
+ throw lastError;
53
+ }
54
+ // --- Core request (no retry) ---
55
+ async function rawRequest(config, options) {
56
+ const url = `${config.baseUrl}${options.path}`;
57
+ const headers = {
58
+ ...gatewayAuthHeaders(),
59
+ 'Accept': 'application/json',
60
+ ...evoHeaders(options.tool ?? 'unknown'),
61
+ };
62
+ if (options.body)
63
+ headers['Content-Type'] = 'application/json';
64
+ if (options.idempotencyKey) {
65
+ headers['Idempotency-Key'] = options.idempotencyKey;
66
+ headers['X-Evo-Run-Id'] = options.idempotencyKey;
67
+ }
68
+ const isRead = options.method === 'GET' || !options.idempotencyKey;
69
+ let response;
70
+ try {
71
+ response = await fetchWithTimeout(url, {
72
+ method: options.method,
73
+ headers,
74
+ body: options.body ? JSON.stringify(options.body) : undefined,
75
+ }, options.timeoutMs ?? timeoutFromEnv(isRead ? 'EVOLINK_MCP_READ_TIMEOUT_MS' : 'EVOLINK_MCP_WRITE_TIMEOUT_MS', isRead ? DEFAULT_READ_TIMEOUT_MS : DEFAULT_SUBMIT_TIMEOUT_MS));
76
+ }
77
+ catch (error) {
78
+ if (!isRead)
79
+ throw new PaidRequestOutcomeUnknownError(error, options.idempotencyKey);
80
+ throw error;
81
+ }
82
+ const data = await readJsonBody(response);
83
+ const requestId = responseRequestId(response.headers);
84
+ if (!response.ok) {
85
+ const retryAfterMs = parseRetryAfter(response.headers.get('retry-after'));
86
+ const info = classifyGatewayError(response.status, data, retryAfterMs, requestId);
87
+ throw new ApiHttpError(response.status, formatGatewayError(info), retryAfterMs, info.request_id, info);
88
+ }
89
+ return { data: data, requestId, headers: response.headers };
90
+ }
91
+ /**
92
+ * Submit one paid intent. At most one transport retry, always with the same
93
+ * idempotency key, so the gateway ledger (Idempotency-Key) can return the
94
+ * original task instead of creating a second one.
95
+ */
96
+ export async function submitTask(config, options) {
97
+ const idempotencyKey = options.idempotencyKey ?? newRunId();
98
+ const { data, requestId, headers } = await withRetry(() => rawRequest(config, {
99
+ method: 'POST',
100
+ path: options.path,
101
+ body: options.body,
102
+ tool: options.tool,
103
+ idempotencyKey,
104
+ }), 1, 500);
105
+ if (!data.request_id && requestId)
106
+ data.request_id = requestId;
107
+ if (headers.get('idempotency-replayed') === 'true')
108
+ data.idempotency_replayed = true;
109
+ return data;
110
+ }
111
+ /** @deprecated kept for callers outside the tools; use submitTask. */
112
+ export async function apiRequest(config, options) {
113
+ if (options.method !== 'POST' || !options.body) {
114
+ throw new Error('apiRequest only accepts generation POST operations');
115
+ }
116
+ return submitTask(config, { path: options.path, body: options.body, tool: options.tool ?? 'unknown' });
117
+ }
118
+ /** One task (GET /v1/tasks/{id}). Unfinished tasks sync with the provider on every read, so callers pace themselves. */
119
+ export async function queryTask(config, taskId, tool = 'get_task') {
120
+ const { data, requestId } = await withRetry(() => rawRequest(config, { method: 'GET', path: `/v1/tasks/${encodeURIComponent(taskId)}`, tool }), 2, 1500);
121
+ if (!data.request_id && requestId)
122
+ data.request_id = requestId;
123
+ return data;
124
+ }
125
+ /** Up to 50 tasks in one call (POST /v1/tasks/batch); unknown or foreign IDs are simply absent. */
126
+ export async function queryTasks(config, taskIds, tool = 'list_tasks') {
127
+ const { data } = await withRetry(() => rawRequest(config, {
128
+ method: 'POST',
129
+ path: '/v1/tasks/batch',
130
+ body: { task_ids: taskIds },
131
+ tool,
132
+ }), 2, 1500);
133
+ return Array.isArray(data.data) ? data.data : [];
134
+ }
135
+ /** The account's recent tasks, newest first (GET /v1/tasks). Items carry no result links. */
136
+ export async function listTasks(config, query, tool = 'list_tasks') {
137
+ const params = new URLSearchParams();
138
+ if (query.status)
139
+ params.set('status', query.status);
140
+ if (query.type)
141
+ params.set('type', query.type);
142
+ if (query.model)
143
+ params.set('model', query.model);
144
+ params.set('page', String(query.page ?? 1));
145
+ params.set('page_size', String(query.pageSize ?? 20));
146
+ const { data } = await withRetry(() => rawRequest(config, { method: 'GET', path: `/v1/tasks?${params}`, tool }), 2, 1500);
147
+ return {
148
+ data: Array.isArray(data.data) ? data.data : [],
149
+ total: typeof data.total === 'number' ? data.total : 0,
150
+ page: typeof data.page === 'number' ? data.page : query.page ?? 1,
151
+ page_size: typeof data.page_size === 'number' ? data.page_size : query.pageSize ?? 20,
152
+ };
153
+ }
154
+ /**
155
+ * A short-lived upload token for a signed-in hosted connection (POST
156
+ * /v1/files/upload-token). The connection has no API key of its own, so
157
+ * files-api takes this token instead. Free; the gateway allows 30 a minute per
158
+ * account.
159
+ */
160
+ export async function requestUploadToken(config, tool) {
161
+ const { data, requestId } = await withRetry(() => rawRequest(config, { method: 'POST', path: '/v1/files/upload-token', tool }), 1, 500);
162
+ const token = typeof data.upload_token === 'string' ? data.upload_token : '';
163
+ const expiresAt = typeof data.expires_at === 'number' ? data.expires_at : 0;
164
+ if (!token.startsWith('evup_') || expiresAt <= 0) {
165
+ const info = classifyGatewayError(500, { error: { message: 'EvoLink returned no upload token.' } }, undefined, requestId);
166
+ throw new ApiHttpError(500, formatGatewayError(info), undefined, requestId, info);
167
+ }
168
+ return { token, expiresAt };
169
+ }
170
+ /** Account balance and this key's usage (GET /v1/credits). */
171
+ export async function getCredits(config, tool = 'check_balance') {
172
+ const { data, requestId } = await withRetry(() => rawRequest(config, { method: 'GET', path: '/v1/credits', tool }), 2, 1000);
173
+ if (data.success === false || !data.data?.user || !data.data?.token) {
174
+ const info = classifyGatewayError(500, { error: { message: data.message || 'The balance could not be read.' } }, undefined, requestId);
175
+ throw new ApiHttpError(500, formatGatewayError(info), undefined, requestId, info);
176
+ }
177
+ return data.data;
178
+ }
179
+ //# sourceMappingURL=api-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"api-client.js","sourceRoot":"","sources":["../../../../../core/src/services/api-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * What the assistant should do about a rejection. The gateway already tells
3
+ * the three quota cases apart by error.code (all are HTTP 402 with the same
4
+ * error.type), so classification goes by code first and status second.
5
+ *
6
+ * A signed-in connection spends the account's internal MCP key, so its key
7
+ * limits are the user's "EvoLink MCP limit" (mcp_*), never an API key's.
8
+ */
9
+ export type ErrorCategory = 'account_balance_insufficient' | 'mcp_limit_reached' | 'mcp_daily_limit_reached' | 'mcp_paused' | 'mcp_key_expired' | 'key_quota_exhausted' | 'key_daily_quota_exhausted' | 'key_disabled' | 'key_expired' | 'model_not_allowed' | 'unauthorized' | 'forbidden' | 'rate_limited' | 'invalid_request' | 'not_found' | 'content_policy' | 'model_unavailable' | 'idempotency_conflict' | 'outcome_unknown' | 'request_too_large' | 'server_error' | 'connection_ended' | 'session_check_unavailable' | 'connection_setup_failed' | 'service_misconfigured' | 'sign_in_token_rejected' | 'mcp_channel_disabled' | 'account_disabled';
10
+ export interface GatewayErrorInfo {
11
+ status: number;
12
+ category: ErrorCategory;
13
+ /**
14
+ * First sentence for the user when a limit, the balance or the connection
15
+ * stopped the request. It names which one, so every client can tell an
16
+ * EvoLink MCP limit from an empty account.
17
+ */
18
+ headline?: string;
19
+ code?: string;
20
+ message: string;
21
+ next_step: string;
22
+ retryable: boolean;
23
+ retry_after_seconds?: number;
24
+ action_url?: string;
25
+ request_id?: string;
26
+ /** Key name, balance and limit figures the gateway attaches to quota rejections (credits). */
27
+ details?: Record<string, unknown>;
28
+ }
29
+ /** Gateway display rate: 1 USD = 6.8 CNY and 1 CNY = 10 credits. */
30
+ export declare const CREDITS_PER_USD = 68;
31
+ export declare function formatCredits(value: number): string;
32
+ export declare function formatUsd(value: number): string;
33
+ /** Classifies one non-2xx gateway or files-api response. */
34
+ export declare function classifyGatewayError(status: number, body: unknown, retryAfterMs?: number, headerRequestId?: string): GatewayErrorInfo;
35
+ /**
36
+ * With a headline the first line says which limit it is; the gateway's own
37
+ * sentence is left out because it speaks of "API key …" even for the MCP key.
38
+ */
39
+ export declare function formatGatewayError(info: GatewayErrorInfo): string;
40
+ export declare function formatApiError(status: number, body: unknown): string;
41
+ export type TaskErrorCode = 'content_policy_violation' | 'invalid_parameters' | 'image_dimension_mismatch' | 'image_processing_error' | 'request_cancelled' | 'resource_not_found' | 'generation_timeout' | 'quota_exceeded' | 'resource_exhausted' | 'generation_failed_no_content' | 'service_error' | 'service_unavailable' | 'unknown_error';
42
+ export interface TaskErrorInfo {
43
+ suggestion: string;
44
+ retryable: boolean;
45
+ }
46
+ export declare function getTaskErrorInfo(code: string): TaskErrorInfo;
47
+ export declare function formatTaskError(error: {
48
+ code?: string;
49
+ message?: string;
50
+ }): string;
51
+ //# sourceMappingURL=error-handler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../../../../../core/src/services/error-handler.ts"],"names":[],"mappings":"AAMA;;;;;;;GAOG;AACH,MAAM,MAAM,aAAa,GACrB,8BAA8B,GAC9B,mBAAmB,GACnB,yBAAyB,GACzB,YAAY,GACZ,iBAAiB,GACjB,qBAAqB,GACrB,2BAA2B,GAC3B,cAAc,GACd,aAAa,GACb,mBAAmB,GACnB,cAAc,GACd,WAAW,GACX,cAAc,GACd,iBAAiB,GACjB,WAAW,GACX,gBAAgB,GAChB,mBAAmB,GACnB,sBAAsB,GACtB,iBAAiB,GACjB,mBAAmB,GACnB,cAAc,GACd,kBAAkB,GAClB,2BAA2B,GAC3B,yBAAyB,GACzB,uBAAuB,GACvB,wBAAwB,GACxB,sBAAsB,GACtB,kBAAkB,CAAC;AAEvB,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,aAAa,CAAC;IACxB;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,OAAO,CAAC;IACnB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,8FAA8F;IAC9F,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACnC;AAED,oEAAoE;AACpE,eAAO,MAAM,eAAe,KAAK,CAAC;AAwHlC,wBAAgB,aAAa,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEnD;AAED,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAK/C;AA6JD,4DAA4D;AAC5D,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,IAAI,EAAE,OAAO,EACb,YAAY,CAAC,EAAE,MAAM,EACrB,eAAe,CAAC,EAAE,MAAM,GACvB,gBAAgB,CAuClB;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,gBAAgB,GAAG,MAAM,CAOjE;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,CAEpE;AAID,MAAM,MAAM,aAAa,GACrB,0BAA0B,GAC1B,oBAAoB,GACpB,0BAA0B,GAC1B,wBAAwB,GACxB,mBAAmB,GACnB,oBAAoB,GACpB,oBAAoB,GACpB,gBAAgB,GAChB,oBAAoB,GACpB,8BAA8B,GAC9B,eAAe,GACf,qBAAqB,GACrB,eAAe,CAAC;AAEpB,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,OAAO,CAAC;CACpB;AAyDD,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,CAM5D;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAUlF"}
@@ -0,0 +1,403 @@
1
+ import { currentCredentialMode } from '../request-context.js';
2
+ import { API_KEYS_URL, MCP_CONSOLE_URL, MCP_KEY_NAME, TOP_UP_URL, siteUrl } from './http-policy.js';
3
+ import { trackedLink } from './utm.js';
4
+ /** Gateway display rate: 1 USD = 6.8 CNY and 1 CNY = 10 credits. */
5
+ export const CREDITS_PER_USD = 68;
6
+ const ACCOUNT_CODES = new Set(['insufficient_quota', 'insufficient_user_quota', 'quota_not_enough', 'account_balance_insufficient']);
7
+ const KEY_QUOTA_CODES = new Set(['insufficient_token_quota', 'key_quota_exhausted']);
8
+ const KEY_DAILY_CODES = new Set(['token_daily_quota_exceeded', 'key_daily_quota_exhausted']);
9
+ const CONTENT_CODES = new Set(['content_policy_violation', 'content_filter', 'sensitive_content', 'moderation_blocked', 'input_moderation_failed']);
10
+ const UNAVAILABLE_CODES = new Set(['no_available_channel', 'model_unavailable', 'channel_selection_failed', 'service_unavailable']);
11
+ /** Hosted service channel (key custody A): the connection itself is gone and needs a new sign-in. */
12
+ const CONNECTION_ENDED_CODES = new Set(['connection_not_found', 'connection_revoked', 'session_inactive', 'session_expired', 'mcp_scope_missing']);
13
+ /** The hosted service, not the user's connection, was refused or sent a request the channel does not serve. */
14
+ const SERVICE_CODES = new Set(['mcp_service_unauthorized', 'mcp_connection_required', 'mcp_route_not_allowed']);
15
+ const DETAIL_FIELDS = [
16
+ 'key_name',
17
+ 'estimated_credits',
18
+ 'account_balance_credits',
19
+ 'total_limit_credits',
20
+ 'used_credits',
21
+ 'remaining_credits',
22
+ 'daily_limit_credits',
23
+ 'daily_used_credits',
24
+ 'reset_timezone',
25
+ 'allowed_models',
26
+ 'expired_at',
27
+ ];
28
+ function envelope(body) {
29
+ if (!body || typeof body !== 'object')
30
+ return {};
31
+ const record = body;
32
+ if (record.error && typeof record.error === 'object')
33
+ return record.error;
34
+ // files-api: { success: false, code, msg }; OAuth-style: { error: "x", error_description: "…" }
35
+ return {
36
+ code: typeof record.error === 'string' ? record.error : record.code,
37
+ message: record.msg ?? record.error_description ?? record.message,
38
+ };
39
+ }
40
+ function text(value) {
41
+ if (typeof value === 'string' && value.trim())
42
+ return value.trim();
43
+ if (typeof value === 'number' && Number.isFinite(value))
44
+ return String(value);
45
+ return undefined;
46
+ }
47
+ /**
48
+ * @param mcpLimits the key limits are the account's EvoLink MCP limit: a
49
+ * signed-in connection, or the gateway said so with limit_scope "mcp".
50
+ */
51
+ function categorize(status, code, mode, mcpLimits) {
52
+ const lower = code.toLowerCase();
53
+ if (ACCOUNT_CODES.has(lower))
54
+ return 'account_balance_insufficient';
55
+ if (KEY_QUOTA_CODES.has(lower))
56
+ return mcpLimits ? 'mcp_limit_reached' : 'key_quota_exhausted';
57
+ if (KEY_DAILY_CODES.has(lower))
58
+ return mcpLimits ? 'mcp_daily_limit_reached' : 'key_daily_quota_exhausted';
59
+ if (lower === 'mcp_paused')
60
+ return 'mcp_paused';
61
+ if (lower === 'mcp_key_expired')
62
+ return 'mcp_key_expired';
63
+ // EvoLink turned the MCP channel off (the gateway's MCPChannelEnabled switch): a 503, but not a model problem.
64
+ if (lower === 'mcp_channel_disabled')
65
+ return 'mcp_channel_disabled';
66
+ // The account's MCP key is only ever disabled by "pause all" in the console.
67
+ if (lower === 'key_disabled')
68
+ return mode === 'signed_in' ? 'mcp_paused' : 'key_disabled';
69
+ // A signed-in connection spends the account's MCP key, so an expired key is that key's expiry date, not the connection.
70
+ if (lower === 'key_expired')
71
+ return mode === 'signed_in' ? 'mcp_key_expired' : 'key_expired';
72
+ if (lower === 'key_model_not_allowed')
73
+ return 'model_not_allowed';
74
+ if (lower === 'idempotency_conflict')
75
+ return 'idempotency_conflict';
76
+ if (lower === 'paid_outcome_unknown')
77
+ return 'outcome_unknown';
78
+ if (CONTENT_CODES.has(lower))
79
+ return 'content_policy';
80
+ if (UNAVAILABLE_CODES.has(lower) || lower.startsWith('channel:'))
81
+ return 'model_unavailable';
82
+ if (CONNECTION_ENDED_CODES.has(lower))
83
+ return 'connection_ended';
84
+ if (lower === 'agent_session_unavailable')
85
+ return 'session_check_unavailable';
86
+ if (lower === 'mcp_connection_create_failed')
87
+ return 'connection_setup_failed';
88
+ if (SERVICE_CODES.has(lower))
89
+ return 'service_misconfigured';
90
+ // The gateway did not take the forwarded sign-in token, most likely because it expired during the call.
91
+ if (lower === 'mcp_token_invalid')
92
+ return 'sign_in_token_rejected';
93
+ if (lower === 'user_disabled')
94
+ return 'account_disabled';
95
+ // Upload tokens for signed-in connections (POST /v1/files/upload-token): a per-minute cap, and Redis being down.
96
+ if (lower === 'upload_token_rate_limited')
97
+ return 'rate_limited';
98
+ if (lower === 'upload_token_unavailable')
99
+ return 'server_error';
100
+ switch (status) {
101
+ case 401: return 'unauthorized';
102
+ case 402: return 'account_balance_insufficient';
103
+ case 403: return 'forbidden';
104
+ case 404: return 'not_found';
105
+ case 413: return 'request_too_large';
106
+ case 429: return 'rate_limited';
107
+ case 503: return 'model_unavailable';
108
+ }
109
+ if (status >= 500)
110
+ return 'server_error';
111
+ return 'invalid_request';
112
+ }
113
+ const RETRYABLE = new Set([
114
+ 'key_daily_quota_exhausted',
115
+ 'mcp_daily_limit_reached',
116
+ 'rate_limited',
117
+ 'model_unavailable',
118
+ 'outcome_unknown',
119
+ 'server_error',
120
+ 'session_check_unavailable',
121
+ 'connection_setup_failed',
122
+ 'sign_in_token_rejected',
123
+ ]);
124
+ /** Answers about the account's EvoLink MCP limit; their console link is the MCP settings, not API Keys. */
125
+ const MCP_LIMIT_CATEGORIES = new Set(['mcp_limit_reached', 'mcp_daily_limit_reached', 'mcp_paused', 'mcp_key_expired']);
126
+ const KEY_CATEGORIES = new Set([
127
+ 'key_quota_exhausted', 'key_daily_quota_exhausted', 'key_disabled', 'key_expired', 'model_not_allowed',
128
+ ]);
129
+ function credits(value) {
130
+ if (typeof value !== 'number' || !Number.isFinite(value))
131
+ return undefined;
132
+ return `${formatCredits(value)} credits (≈$${formatUsd(value / CREDITS_PER_USD)})`;
133
+ }
134
+ export function formatCredits(value) {
135
+ return Number(value.toFixed(4)).toString();
136
+ }
137
+ export function formatUsd(value) {
138
+ if (value === 0)
139
+ return '0';
140
+ if (Math.abs(value) >= 1)
141
+ return value.toFixed(2);
142
+ if (Math.abs(value) >= 0.01)
143
+ return value.toFixed(3);
144
+ return Number(value.toPrecision(2)).toString();
145
+ }
146
+ /** Sentences naming which limit stopped the request; the same words in every client. */
147
+ function headlineFor(category) {
148
+ switch (category) {
149
+ case 'mcp_limit_reached':
150
+ return 'EvoLink MCP spending limit reached. This is the limit set for MCP, not your account balance.';
151
+ case 'mcp_daily_limit_reached':
152
+ return 'EvoLink MCP daily limit reached. It resets at midnight; this is not your account balance.';
153
+ case 'mcp_paused':
154
+ return 'EvoLink MCP is paused for this account. Resume it in the console; you do not need to reconnect.';
155
+ case 'mcp_key_expired':
156
+ return 'EvoLink MCP key has expired. Extend or clear its expiry date in the console; you do not need to reconnect.';
157
+ case 'account_balance_insufficient':
158
+ return 'Your EvoLink account balance is too low for this request. The balance is shared by API, web and MCP.';
159
+ case 'key_quota_exhausted':
160
+ return 'This API key\'s spending limit is used up. This is the key\'s own limit, not your account balance.';
161
+ case 'key_daily_quota_exhausted':
162
+ return 'This API key\'s daily limit is used up. It resets at midnight; this is not your account balance.';
163
+ case 'connection_ended':
164
+ return 'This EvoLink connection was disconnected. Reconnect EvoLink in this client.';
165
+ // The gateway's own text ("Agent session validation…") is shared with the CLI and names an internal check.
166
+ case 'session_check_unavailable':
167
+ return 'EvoLink could not check this connection\'s sign-in just now. Retry in a minute; you do not need to reconnect.';
168
+ default:
169
+ return undefined;
170
+ }
171
+ }
172
+ /**
173
+ * Where the EvoLink MCP limit lives: while it is a key in API Keys, name the key
174
+ * so the user finds it; a dedicated MCP page needs no key name.
175
+ */
176
+ function mcpLimitPlace(url, details) {
177
+ return /\/dashboard\/keys(?:[?#]|$)/.test(url)
178
+ ? `${url} (the key named "${text(details.key_name) ?? MCP_KEY_NAME}")`
179
+ : url;
180
+ }
181
+ /** "Limit: …; used: …; left: …." from whatever figures the gateway attached. */
182
+ function limitFigures(details, label) {
183
+ const parts = [
184
+ credits(details.total_limit_credits) && `${label}: ${credits(details.total_limit_credits)}`,
185
+ credits(details.used_credits) && `used: ${credits(details.used_credits)}`,
186
+ credits(details.remaining_credits) && `left: ${credits(details.remaining_credits)}`,
187
+ ].filter(Boolean);
188
+ return parts.length ? `${parts.join('; ')}.` : '';
189
+ }
190
+ function sentences(...parts) {
191
+ return parts.filter(Boolean).join(' ');
192
+ }
193
+ function nextStep(category, info, mode) {
194
+ const details = info.details ?? {};
195
+ const action = info.action_url;
196
+ const needed = credits(details.estimated_credits);
197
+ const balance = credits(details.account_balance_credits);
198
+ const zone = text(details.reset_timezone);
199
+ switch (category) {
200
+ case 'account_balance_insufficient':
201
+ return sentences(balance && `Balance: ${balance}.`, needed && `This request needs about ${needed}.`, `Ask the user to top up at ${action ?? trackedLink(TOP_UP_URL, 'top_up')}, then retry.`);
202
+ case 'mcp_limit_reached':
203
+ return sentences(limitFigures(details, 'EvoLink MCP limit'), needed && `This request needs about ${needed}.`, balance && `Account balance: ${balance} (not the problem).`, `Ask the user to raise or remove the EvoLink MCP limit at ${mcpLimitPlace(action ?? trackedLink(MCP_CONSOLE_URL, 'api_keys'), details)}; it is shared by every assistant connected to their EvoLink account.`, 'Do not retry until they have changed it.');
204
+ case 'mcp_daily_limit_reached': {
205
+ const used = credits(details.daily_used_credits);
206
+ const limit = credits(details.daily_limit_credits);
207
+ return sentences(used && limit && `Today's EvoLink MCP spending: ${used} of the ${limit} daily limit.`, `It resets automatically at midnight${zone ? ` (${zone})` : ''}.`, balance && `Account balance: ${balance} (not the problem).`, `To continue today, ask the user to raise the EvoLink MCP daily limit at ${mcpLimitPlace(action ?? trackedLink(MCP_CONSOLE_URL, 'api_keys'), details)}.`);
208
+ }
209
+ case 'mcp_paused':
210
+ return `Ask the user to resume EvoLink MCP at ${mcpLimitPlace(action ?? trackedLink(MCP_CONSOLE_URL, 'api_keys'), details)} (switch it back on). Do not ask them to reconnect, and do not retry until it is resumed.`;
211
+ case 'mcp_key_expired': {
212
+ const expiredAt = typeof details.expired_at === 'number' && Number.isFinite(details.expired_at) && details.expired_at > 0
213
+ ? new Date(details.expired_at * 1000).toISOString().slice(0, 10)
214
+ : undefined;
215
+ return sentences(expiredAt && `The EvoLink MCP key expired on ${expiredAt} (UTC).`, `Ask the user to extend or clear the expiry date of the EvoLink MCP key at ${mcpLimitPlace(action ?? trackedLink(MCP_CONSOLE_URL, 'api_keys'), details)}.`, 'Do not ask them to reconnect, and do not retry until it is changed.');
216
+ }
217
+ case 'key_quota_exhausted': {
218
+ const name = text(details.key_name);
219
+ return sentences(limitFigures(details, name ? `Limit of API key "${name}"` : 'API key limit'), needed && `This request needs about ${needed}.`, balance && `Account balance: ${balance} (not the problem).`, `Raise this API key's limit at ${action ?? trackedLink(API_KEYS_URL, 'api_keys')}, or use another key.`);
220
+ }
221
+ case 'key_daily_quota_exhausted':
222
+ return `It resets automatically at midnight${zone ? ` (${zone})` : ''}; to continue today, raise this API key's daily limit at ${action ?? trackedLink(API_KEYS_URL, 'api_keys')}.`;
223
+ case 'key_disabled':
224
+ return `This API key is disabled. Enable it at ${action ?? trackedLink(API_KEYS_URL, 'api_keys')} or use another key.`;
225
+ case 'key_expired':
226
+ return `This API key has expired. Extend it at ${action ?? trackedLink(API_KEYS_URL, 'api_keys')} or use another key.`;
227
+ case 'model_not_allowed': {
228
+ const allowed = Array.isArray(details.allowed_models) ? details.allowed_models.slice(0, 20).join(', ') : '';
229
+ return `This key may not use this model.${allowed ? ` Allowed models: ${allowed}.` : ''} Pick an allowed model, or change the key's model list at ${action ?? trackedLink(API_KEYS_URL, 'api_keys')}.`;
230
+ }
231
+ case 'unauthorized':
232
+ if (mode === 'signed_in')
233
+ return 'The EvoLink connection was rejected. Ask the user to reconnect EvoLink in this client.';
234
+ return mode === 'api_key'
235
+ ? 'The API key was rejected. Ask the user to check the EvoLink API key configured in this client.'
236
+ : 'The API key was rejected. Check EVOLINK_API_KEY, or run `evolink login` again.';
237
+ case 'forbidden':
238
+ return 'Access to this resource is denied for this account.';
239
+ case 'rate_limited':
240
+ return `Too many requests. Wait ${info.retry_after_seconds ?? 30} seconds before retrying; do not loop paid submissions.`;
241
+ case 'invalid_request':
242
+ return 'Fix the parameter named in the message and retry. get_model lists every parameter with its allowed values.';
243
+ case 'not_found':
244
+ return 'Nothing matches this ID. Check it, or use list_tasks to find recent tasks.';
245
+ case 'content_policy':
246
+ return 'The prompt or input was blocked by content review. Rephrase it (no real people, brands, explicit or violent content) and retry.';
247
+ case 'model_unavailable':
248
+ return 'The model is temporarily unavailable. Retry in a minute, or pick another model with search_models.';
249
+ case 'idempotency_conflict':
250
+ return 'This client_request_id was already used for a different request. Use a new client_request_id for a new generation.';
251
+ case 'outcome_unknown':
252
+ return 'The earlier submission with this client_request_id is still being processed or its outcome is unknown. Do not submit it again with a new id: wait a minute and retry with the same client_request_id, or look it up with list_tasks.';
253
+ case 'request_too_large':
254
+ return 'The request is too large. Pass big files as links (a public URL, or one from upload_file where it is available) instead of inline data.';
255
+ case 'server_error':
256
+ return 'EvoLink had a temporary error. Retry in a minute; for paid generations check list_tasks first so the task is not submitted twice.';
257
+ case 'connection_ended':
258
+ return 'This EvoLink connection is no longer active (it was revoked, expired, or signed out). Ask the user to reconnect the existing EvoLink connection in this client instead of adding a new one.';
259
+ case 'session_check_unavailable':
260
+ return 'EvoLink sign-in verification is temporarily unavailable. Retry in a minute; do not reconnect.';
261
+ case 'connection_setup_failed':
262
+ return 'EvoLink could not set up this connection yet. Retry in a minute.';
263
+ case 'service_misconfigured':
264
+ return 'The EvoLink MCP service could not authenticate this request. This is a server-side problem, not the user\'s connection: do not reconnect, and try again later.';
265
+ case 'mcp_channel_disabled':
266
+ return 'EvoLink has temporarily turned off MCP access. Try again later; switching models, retrying right away or reconnecting will not help.';
267
+ case 'sign_in_token_rejected':
268
+ return 'EvoLink did not accept this connection\'s sign-in token; it may have just expired. Retry the call, which carries a refreshed token; if it keeps failing, ask the user to reconnect the existing EvoLink connection in this client.';
269
+ case 'account_disabled':
270
+ return 'This EvoLink account is disabled. Ask the user to contact EvoLink support.';
271
+ }
272
+ }
273
+ /** Classifies one non-2xx gateway or files-api response. */
274
+ export function classifyGatewayError(status, body, retryAfterMs, headerRequestId) {
275
+ const error = envelope(body);
276
+ const code = text(error.code);
277
+ const mode = currentCredentialMode();
278
+ const mcpScope = text(error.limit_scope)?.toLowerCase() === 'mcp';
279
+ const category = categorize(status, code ?? '', mode, mcpScope || mode === 'signed_in');
280
+ const requestId = text(error.request_id) ?? headerRequestId;
281
+ const rawMessage = text(error.message) ?? `HTTP ${status}`;
282
+ const message = rawMessage.replace(/\s*\(request id: [^)]*\)\s*$/i, '').slice(0, 600);
283
+ const details = {};
284
+ for (const field of DETAIL_FIELDS) {
285
+ if (error[field] !== undefined && error[field] !== null && error[field] !== '')
286
+ details[field] = error[field];
287
+ }
288
+ let actionUrl;
289
+ if (category === 'account_balance_insufficient') {
290
+ actionUrl = trackedLink(siteUrl(text(error.action_url), TOP_UP_URL), 'top_up');
291
+ }
292
+ else if (MCP_LIMIT_CATEGORIES.has(category)) {
293
+ actionUrl = trackedLink(siteUrl(text(error.action_url), MCP_CONSOLE_URL), 'api_keys');
294
+ }
295
+ else if (KEY_CATEGORIES.has(category)) {
296
+ actionUrl = trackedLink(siteUrl(text(error.action_url), API_KEYS_URL), 'api_keys');
297
+ }
298
+ const headline = headlineFor(category);
299
+ const info = {
300
+ status,
301
+ category,
302
+ ...(headline ? { headline } : {}),
303
+ code,
304
+ message,
305
+ next_step: '',
306
+ retryable: RETRYABLE.has(category),
307
+ retry_after_seconds: retryAfterMs !== undefined ? Math.max(1, Math.ceil(retryAfterMs / 1000)) : undefined,
308
+ action_url: actionUrl,
309
+ request_id: requestId,
310
+ details: Object.keys(details).length > 0 ? details : undefined,
311
+ };
312
+ info.next_step = nextStep(category, info, mode);
313
+ return info;
314
+ }
315
+ /**
316
+ * With a headline the first line says which limit it is; the gateway's own
317
+ * sentence is left out because it speaks of "API key …" even for the MCP key.
318
+ */
319
+ export function formatGatewayError(info) {
320
+ const status = `${info.status}${info.code ? ` ${info.code}` : ''}`;
321
+ const lines = info.headline
322
+ ? [info.headline, `Next step: ${info.next_step}`, `Error: HTTP ${status}`]
323
+ : [`[${status}] ${info.message}`, `Next step: ${info.next_step}`];
324
+ if (info.request_id)
325
+ lines.push(`Request ID: ${info.request_id}`);
326
+ return lines.join('\n');
327
+ }
328
+ export function formatApiError(status, body) {
329
+ return formatGatewayError(classifyGatewayError(status, body));
330
+ }
331
+ const TASK_ERROR_MAP = {
332
+ content_policy_violation: {
333
+ suggestion: 'Revise the prompt: avoid real person photos, celebrity names, copyrighted content, NSFW or violence. An illustration style often passes.',
334
+ retryable: false,
335
+ },
336
+ invalid_parameters: {
337
+ suggestion: 'Check the parameter values (prompt length, image size, duration, resolution) against get_model.',
338
+ retryable: false,
339
+ },
340
+ image_dimension_mismatch: {
341
+ suggestion: 'The input image does not match the requested aspect ratio. Resize it (for example 1280x720 for 16:9) or change the ratio.',
342
+ retryable: false,
343
+ },
344
+ image_processing_error: {
345
+ suggestion: 'The input image could not be processed. Use JPG, PNG or WebP under 10 MB at a publicly reachable URL (upload_file gives one).',
346
+ retryable: false,
347
+ },
348
+ request_cancelled: {
349
+ suggestion: 'The task was cancelled. Submit a new request if it was not intended.',
350
+ retryable: false,
351
+ },
352
+ resource_not_found: {
353
+ suggestion: 'The task or an input resource was not found or has expired. Check the IDs and links.',
354
+ retryable: false,
355
+ },
356
+ generation_timeout: {
357
+ suggestion: 'Generation timed out, probably under high load. Retry, or simplify the prompt or lower the resolution.',
358
+ retryable: true,
359
+ },
360
+ quota_exceeded: {
361
+ suggestion: `The account was over quota or rate limited when the task ran. Wait, then retry; top up at ${TOP_UP_URL} if the balance is low.`,
362
+ retryable: true,
363
+ },
364
+ resource_exhausted: {
365
+ suggestion: 'Provider capacity was temporarily exhausted. Wait 30–60 seconds and retry.',
366
+ retryable: true,
367
+ },
368
+ generation_failed_no_content: {
369
+ suggestion: 'The model produced no output, often because of protected content or watermark removal requests. Change the prompt or input and retry.',
370
+ retryable: true,
371
+ },
372
+ service_error: {
373
+ suggestion: 'Temporary service error. Retry after a minute.',
374
+ retryable: true,
375
+ },
376
+ service_unavailable: {
377
+ suggestion: 'The service was temporarily unavailable. Retry after 1–2 minutes.',
378
+ retryable: true,
379
+ },
380
+ unknown_error: {
381
+ suggestion: 'Unknown error. Retry after a minute; if it keeps failing, give the task ID to EvoLink support.',
382
+ retryable: true,
383
+ },
384
+ };
385
+ export function getTaskErrorInfo(code) {
386
+ const info = TASK_ERROR_MAP[code] ?? TASK_ERROR_MAP.unknown_error;
387
+ // The top-up link is tagged for the caller when the hint is shown, not when the table is built.
388
+ return info.suggestion.includes(TOP_UP_URL)
389
+ ? { ...info, suggestion: info.suggestion.replace(TOP_UP_URL, trackedLink(TOP_UP_URL, 'top_up')) }
390
+ : info;
391
+ }
392
+ export function formatTaskError(error) {
393
+ const code = error.code ?? 'unknown_error';
394
+ const info = getTaskErrorInfo(code);
395
+ const lines = [
396
+ `Error code: ${code}`,
397
+ `Message: ${error.message ?? 'No details provided'}`,
398
+ `Retryable: ${info.retryable ? 'Yes — you can retry this request' : 'No — modify your input before retrying'}`,
399
+ `Suggestion: ${info.suggestion}`,
400
+ ];
401
+ return lines.join('\n');
402
+ }
403
+ //# sourceMappingURL=error-handler.js.map