btca-server 1.0.71 → 1.0.80

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.
package/src/index.ts CHANGED
@@ -28,7 +28,6 @@ import { VirtualFs } from './vfs/virtual-fs.ts';
28
28
  * GET /resources - Lists all configured resources
29
29
  * POST /question - Ask a question (non-streaming)
30
30
  * POST /question/stream - Ask a question (streaming SSE response)
31
- * POST /opencode - Get OpenCode instance URL for a collection
32
31
  */
33
32
 
34
33
  // ─────────────────────────────────────────────────────────────────────────────
@@ -82,17 +81,6 @@ const QuestionRequestSchema = z.object({
82
81
  quiet: z.boolean().optional()
83
82
  });
84
83
 
85
- const OpencodeRequestSchema = z.object({
86
- resources: z
87
- .array(ResourceNameField)
88
- .max(
89
- LIMITS.MAX_RESOURCES_PER_REQUEST,
90
- `Too many resources (max ${LIMITS.MAX_RESOURCES_PER_REQUEST})`
91
- )
92
- .optional(),
93
- quiet: z.boolean().optional()
94
- });
95
-
96
84
  const UpdateModelRequestSchema = z.object({
97
85
  provider: z
98
86
  .string()
@@ -120,10 +108,30 @@ const AddGitResourceRequestSchema = z.object({
120
108
  specialNotes: GitResourceSchema.shape.specialNotes
121
109
  });
122
110
 
111
+ const isWsl = () =>
112
+ process.platform === 'linux' &&
113
+ (Boolean(process.env.WSL_DISTRO_NAME) ||
114
+ Boolean(process.env.WSL_INTEROP) ||
115
+ Boolean(process.env.WSLENV));
116
+
117
+ const normalizeWslPath = (value: string) => {
118
+ if (!isWsl()) return value;
119
+ const match = value.match(/^([a-zA-Z]):\\(.*)$/);
120
+ if (!match) return value;
121
+ const drive = match[1]!.toLowerCase();
122
+ const rest = match[2]!.replace(/\\/g, '/');
123
+ return `/mnt/${drive}/${rest}`;
124
+ };
125
+
126
+ const LocalPathRequestSchema = z.preprocess(
127
+ (value) => (typeof value === 'string' ? normalizeWslPath(value) : value),
128
+ LocalResourceSchema.shape.path
129
+ ) as z.ZodType<string>;
130
+
123
131
  const AddLocalResourceRequestSchema = z.object({
124
132
  type: z.literal('local'),
125
133
  name: LocalResourceSchema.shape.name,
126
- path: LocalResourceSchema.shape.path,
134
+ path: LocalPathRequestSchema,
127
135
  specialNotes: LocalResourceSchema.shape.specialNotes
128
136
  });
129
137
 
@@ -361,58 +369,6 @@ const createApp = (deps: {
361
369
  });
362
370
  })
363
371
 
364
- // POST /opencode - Get OpenCode instance URL for a collection
365
- .post('/opencode', async (c: HonoContext) => {
366
- const decoded = await decodeJson(c.req.raw, OpencodeRequestSchema);
367
- const resourceNames =
368
- decoded.resources && decoded.resources.length > 0
369
- ? decoded.resources
370
- : config.resources.map((r) => r.name);
371
-
372
- const collectionKey = getCollectionKey(resourceNames);
373
- Metrics.info('opencode.requested', {
374
- quiet: decoded.quiet ?? false,
375
- resources: resourceNames,
376
- collectionKey
377
- });
378
-
379
- const collection = await collections.load({ resourceNames, quiet: decoded.quiet });
380
- Metrics.info('collection.ready', { collectionKey, path: collection.path });
381
-
382
- const { url, model, instanceId } = await agent.getOpencodeInstance({ collection });
383
- Metrics.info('opencode.ready', { collectionKey, url, instanceId });
384
-
385
- return c.json({
386
- url,
387
- model,
388
- instanceId,
389
- resources: resourceNames,
390
- collection: { key: collectionKey, path: collection.path }
391
- });
392
- })
393
-
394
- // GET /opencode/instances - List all active OpenCode instances
395
- .get('/opencode/instances', (c: HonoContext) => {
396
- const instances = agent.listInstances();
397
- return c.json({ instances, count: instances.length });
398
- })
399
-
400
- // DELETE /opencode/instances - Close all OpenCode instances
401
- .delete('/opencode/instances', async (c: HonoContext) => {
402
- const result = await agent.closeAllInstances();
403
- return c.json(result);
404
- })
405
-
406
- // DELETE /opencode/:id - Close a specific OpenCode instance
407
- .delete('/opencode/:id', async (c: HonoContext) => {
408
- const instanceId = c.req.param('id');
409
- const result = await agent.closeInstance(instanceId);
410
- if (!result.closed) {
411
- return c.json({ error: 'Instance not found', instanceId }, 404);
412
- }
413
- return c.json({ closed: true, instanceId });
414
- })
415
-
416
372
  // PUT /config/model - Update model configuration
417
373
  .put('/config/model', async (c: HonoContext) => {
418
374
  const decoded = await decodeJson(c.req.raw, UpdateModelRequestSchema);
@@ -529,7 +485,6 @@ export const startServer = async (options: StartServerOptions = {}): Promise<Ser
529
485
  port: actualPort,
530
486
  url: `http://localhost:${actualPort}`,
531
487
  stop: () => {
532
- void agent.closeAllInstances();
533
488
  VirtualFs.disposeAll();
534
489
  clearAllVirtualCollectionMetadata();
535
490
  server.stop();
@@ -13,9 +13,30 @@ import { z } from 'zod';
13
13
  import { Result } from 'better-result';
14
14
 
15
15
  export namespace Auth {
16
- const getOpenRouterApiKey = () => {
17
- const apiKey = process.env.OPENROUTER_API_KEY;
18
- return apiKey && apiKey.trim().length > 0 ? apiKey.trim() : undefined;
16
+ export type AuthType = 'api' | 'oauth' | 'wellknown';
17
+
18
+ export type AuthStatus =
19
+ | { status: 'ok'; authType: AuthType; apiKey?: string; accountId?: string }
20
+ | { status: 'missing' }
21
+ | { status: 'invalid'; authType: AuthType };
22
+
23
+ const PROVIDER_AUTH_TYPES: Record<string, readonly AuthType[]> = {
24
+ opencode: ['api'],
25
+ openrouter: ['api'],
26
+ openai: ['oauth'],
27
+ anthropic: ['api'],
28
+ google: ['api', 'oauth']
29
+ };
30
+
31
+ const readEnv = (key: string) => {
32
+ const value = process.env[key];
33
+ return value && value.trim().length > 0 ? value.trim() : undefined;
34
+ };
35
+
36
+ const getEnvApiKey = (providerId: string) => {
37
+ if (providerId === 'openrouter') return readEnv('OPENROUTER_API_KEY');
38
+ if (providerId === 'opencode') return readEnv('OPENCODE_API_KEY');
39
+ return undefined;
19
40
  };
20
41
 
21
42
  // Auth schema matching OpenCode's format
@@ -28,7 +49,8 @@ export namespace Auth {
28
49
  type: z.literal('oauth'),
29
50
  access: z.string(),
30
51
  refresh: z.string(),
31
- expires: z.number()
52
+ expires: z.number(),
53
+ accountId: z.string().optional()
32
54
  });
33
55
 
34
56
  const WellKnownAuthSchema = z.object({
@@ -106,13 +128,52 @@ export namespace Auth {
106
128
  return authData[providerId];
107
129
  }
108
130
 
131
+ export async function getAuthStatus(providerId: string): Promise<AuthStatus> {
132
+ const allowedTypes = PROVIDER_AUTH_TYPES[providerId];
133
+ if (!allowedTypes) return { status: 'missing' };
134
+
135
+ const envKey = getEnvApiKey(providerId);
136
+ if (envKey) {
137
+ return allowedTypes.includes('api')
138
+ ? { status: 'ok', authType: 'api', apiKey: envKey }
139
+ : { status: 'invalid', authType: 'api' };
140
+ }
141
+
142
+ const auth = await getCredentials(providerId);
143
+ if (!auth) return { status: 'missing' };
144
+
145
+ if (!allowedTypes.includes(auth.type)) {
146
+ return { status: 'invalid', authType: auth.type };
147
+ }
148
+
149
+ const apiKey = auth.type === 'api' ? auth.key : auth.type === 'oauth' ? auth.access : undefined;
150
+ const accountId = auth.type === 'oauth' ? auth.accountId : undefined;
151
+ return { status: 'ok', authType: auth.type, apiKey, accountId };
152
+ }
153
+
154
+ export const getProviderAuthHint = (providerId: string) => {
155
+ switch (providerId) {
156
+ case 'openai':
157
+ return 'Run "opencode auth --provider openai" and complete OAuth.';
158
+ case 'anthropic':
159
+ return 'Run "opencode auth --provider anthropic" and enter an API key.';
160
+ case 'google':
161
+ return 'Run "opencode auth --provider google" and enter an API key or OAuth.';
162
+ case 'openrouter':
163
+ return 'Set OPENROUTER_API_KEY or run "opencode auth --provider openrouter".';
164
+ case 'opencode':
165
+ return 'Set OPENCODE_API_KEY or run "opencode auth --provider opencode".';
166
+ default:
167
+ return 'Run "opencode auth --provider <provider>" to configure credentials.';
168
+ }
169
+ };
170
+
109
171
  /**
110
172
  * Check if a provider is authenticated
111
173
  */
112
174
  export async function isAuthenticated(providerId: string): Promise<boolean> {
113
- if (providerId === 'openrouter' && getOpenRouterApiKey()) return true;
114
- const auth = await getCredentials(providerId);
115
- return auth !== undefined;
175
+ const status = await getAuthStatus(providerId);
176
+ return status.status === 'ok';
116
177
  }
117
178
 
118
179
  /**
@@ -120,24 +181,9 @@ export namespace Auth {
120
181
  * Returns undefined if not authenticated or no key available
121
182
  */
122
183
  export async function getApiKey(providerId: string): Promise<string | undefined> {
123
- if (providerId === 'openrouter') {
124
- const envKey = getOpenRouterApiKey();
125
- if (envKey) return envKey;
126
- }
127
-
128
- const auth = await getCredentials(providerId);
129
- if (!auth) return undefined;
130
-
131
- if (auth.type === 'api') {
132
- return auth.key;
133
- }
134
-
135
- if (auth.type === 'oauth') {
136
- return auth.access;
137
- }
138
-
139
- // wellknown auth doesn't have an API key
140
- return undefined;
184
+ const status = await getAuthStatus(providerId);
185
+ if (status.status !== 'ok') return undefined;
186
+ return status.apiKey;
141
187
  }
142
188
 
143
189
  /**
@@ -147,20 +193,22 @@ export namespace Auth {
147
193
  return readAuthFile();
148
194
  }
149
195
 
196
+ /**
197
+ * Update stored credentials for a provider
198
+ */
199
+ export async function setCredentials(providerId: string, info: AuthInfo): Promise<void> {
200
+ const filepath = getAuthFilePath();
201
+ const existing = await readAuthFile();
202
+ const next = { ...existing, [providerId]: info };
203
+ await Bun.write(filepath, JSON.stringify(next, null, 2), { mode: 0o600 });
204
+ }
205
+
150
206
  /**
151
207
  * Get the list of all authenticated provider IDs
152
208
  */
153
209
  export async function getAuthenticatedProviders(): Promise<string[]> {
154
- const authData = await readAuthFile();
155
- const providers = new Set(Object.keys(authData));
156
-
157
- if (getOpenRouterApiKey()) {
158
- providers.add('openrouter');
159
- }
160
- if (authData['openrouter.ai'] || authData['openrouter-ai']) {
161
- providers.add('openrouter');
162
- }
163
-
164
- return Array.from(providers);
210
+ const providers = Object.keys(PROVIDER_AUTH_TYPES);
211
+ const statuses = await Promise.all(providers.map((provider) => getAuthStatus(provider)));
212
+ return providers.filter((_, index) => statuses[index]?.status === 'ok');
165
213
  }
166
214
  }
@@ -6,7 +6,6 @@ export { Auth } from './auth.ts';
6
6
  export { Model } from './model.ts';
7
7
  export {
8
8
  PROVIDER_REGISTRY,
9
- PROVIDER_ALIASES,
10
9
  isProviderSupported,
11
10
  normalizeProviderId,
12
11
  getProviderFactory,
@@ -16,24 +16,39 @@ export namespace Model {
16
16
  export class ProviderNotFoundError extends Error {
17
17
  readonly _tag = 'ProviderNotFoundError';
18
18
  readonly providerId: string;
19
+ readonly hint: string;
19
20
 
20
21
  constructor(providerId: string) {
21
22
  super(`Provider "${providerId}" is not supported`);
22
23
  this.providerId = providerId;
24
+ this.hint =
25
+ 'Open an issue to request this provider: https://github.com/davis7dotsh/better-context/issues.';
23
26
  }
24
27
  }
25
28
 
26
29
  export class ProviderNotAuthenticatedError extends Error {
27
30
  readonly _tag = 'ProviderNotAuthenticatedError';
28
31
  readonly providerId: string;
32
+ readonly hint: string;
29
33
 
30
34
  constructor(providerId: string) {
31
- super(
32
- providerId === 'openrouter'
33
- ? `Provider "${providerId}" is not authenticated. Set OPENROUTER_API_KEY to authenticate.`
34
- : `Provider "${providerId}" is not authenticated. Run 'opencode auth login' to authenticate.`
35
- );
35
+ super(`Provider "${providerId}" is not authenticated.`);
36
36
  this.providerId = providerId;
37
+ this.hint = Auth.getProviderAuthHint(providerId);
38
+ }
39
+ }
40
+
41
+ export class ProviderAuthTypeError extends Error {
42
+ readonly _tag = 'ProviderAuthTypeError';
43
+ readonly providerId: string;
44
+ readonly authType: string;
45
+ readonly hint: string;
46
+
47
+ constructor(args: { providerId: string; authType: string }) {
48
+ super(`Provider "${args.providerId}" does not support "${args.authType}" auth.`);
49
+ this.providerId = args.providerId;
50
+ this.authType = args.authType;
51
+ this.hint = Auth.getProviderAuthHint(args.providerId);
37
52
  }
38
53
  }
39
54
 
@@ -72,22 +87,24 @@ export namespace Model {
72
87
 
73
88
  // Get authentication
74
89
  let apiKey: string | undefined;
90
+ let accountId: string | undefined;
75
91
 
76
92
  if (!options.skipAuth) {
77
- // Special handling for 'opencode' provider - check env var first
78
- if (normalizedProviderId === 'opencode') {
79
- apiKey = process.env.OPENCODE_API_KEY || (await Auth.getApiKey(normalizedProviderId));
80
- } else {
81
- apiKey = await Auth.getApiKey(normalizedProviderId);
82
- if (!apiKey) {
83
- throw new ProviderNotAuthenticatedError(providerId);
84
- }
93
+ const status = await Auth.getAuthStatus(normalizedProviderId);
94
+ if (status.status === 'missing') {
95
+ throw new ProviderNotAuthenticatedError(providerId);
85
96
  }
97
+ if (status.status === 'invalid') {
98
+ throw new ProviderAuthTypeError({ providerId, authType: status.authType });
99
+ }
100
+ apiKey = status.apiKey;
101
+ accountId = status.accountId;
86
102
  }
87
103
 
88
104
  // Build provider options
89
105
  const providerOptions: ProviderOptions = {
90
- ...options.providerOptions
106
+ ...options.providerOptions,
107
+ ...(accountId ? { accountId } : {})
91
108
  };
92
109
 
93
110
  if (apiKey) {
@@ -111,11 +128,6 @@ export namespace Model {
111
128
  return false;
112
129
  }
113
130
 
114
- // Special case: opencode gateway is always available
115
- if (normalizedProviderId === 'opencode') {
116
- return true;
117
- }
118
-
119
131
  return Auth.isAuthenticated(normalizedProviderId);
120
132
  }
121
133
 
@@ -0,0 +1,220 @@
1
+ import { createOpenAI } from '@ai-sdk/openai';
2
+ import * as os from 'node:os';
3
+ import { Auth } from './auth.ts';
4
+
5
+ const CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
6
+ const ISSUER = 'https://auth.openai.com';
7
+ const CODEX_API_ENDPOINT = 'https://chatgpt.com/backend-api/codex/responses';
8
+ const USER_AGENT = `btca/${process.env.npm_package_version ?? 'dev'} (${os.platform()} ${os.release()}; ${os.arch()})`;
9
+
10
+ type TokenResponse = {
11
+ id_token?: string;
12
+ access_token: string;
13
+ refresh_token?: string;
14
+ expires_in?: number;
15
+ };
16
+
17
+ type IdTokenClaims = {
18
+ chatgpt_account_id?: string;
19
+ organizations?: Array<{ id: string }>;
20
+ email?: string;
21
+ 'https://api.openai.com/auth'?: {
22
+ chatgpt_account_id?: string;
23
+ };
24
+ };
25
+
26
+ const parseJwtClaims = (token: string): IdTokenClaims | undefined => {
27
+ const parts = token.split('.');
28
+ if (parts.length !== 3 || !parts[1]) return undefined;
29
+ try {
30
+ return JSON.parse(Buffer.from(parts[1], 'base64url').toString()) as IdTokenClaims;
31
+ } catch {
32
+ return undefined;
33
+ }
34
+ };
35
+
36
+ const extractAccountIdFromClaims = (claims: IdTokenClaims): string | undefined =>
37
+ claims.chatgpt_account_id ||
38
+ claims['https://api.openai.com/auth']?.chatgpt_account_id ||
39
+ claims.organizations?.[0]?.id;
40
+
41
+ const extractAccountId = (tokens: TokenResponse): string | undefined => {
42
+ if (tokens.id_token) {
43
+ const claims = parseJwtClaims(tokens.id_token);
44
+ const accountId = claims && extractAccountIdFromClaims(claims);
45
+ if (accountId) return accountId;
46
+ }
47
+ if (tokens.access_token) {
48
+ const claims = parseJwtClaims(tokens.access_token);
49
+ return claims ? extractAccountIdFromClaims(claims) : undefined;
50
+ }
51
+ return undefined;
52
+ };
53
+
54
+ const refreshAccessToken = async (refreshToken: string): Promise<TokenResponse> => {
55
+ const response = await fetch(`${ISSUER}/oauth/token`, {
56
+ method: 'POST',
57
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
58
+ body: new URLSearchParams({
59
+ grant_type: 'refresh_token',
60
+ refresh_token: refreshToken,
61
+ client_id: CLIENT_ID
62
+ }).toString()
63
+ });
64
+ if (!response.ok) {
65
+ throw new Error(`Token refresh failed: ${response.status}`);
66
+ }
67
+ return response.json() as Promise<TokenResponse>;
68
+ };
69
+
70
+ const buildHeaders = (initHeaders: unknown, accountId?: string): Headers => {
71
+ const headers = new Headers();
72
+
73
+ if (initHeaders instanceof Headers) {
74
+ initHeaders.forEach((value, key) => headers.set(key, value));
75
+ } else if (Array.isArray(initHeaders)) {
76
+ for (const [key, value] of initHeaders) {
77
+ if (value !== undefined) headers.set(key, String(value));
78
+ }
79
+ } else if (initHeaders && typeof initHeaders === 'object') {
80
+ for (const [key, value] of Object.entries(initHeaders as Record<string, unknown>)) {
81
+ if (value !== undefined) headers.set(key, String(value));
82
+ }
83
+ }
84
+
85
+ if (accountId) {
86
+ headers.set('ChatGPT-Account-Id', accountId);
87
+ }
88
+
89
+ return headers;
90
+ };
91
+
92
+ const rewriteUrl = (requestInput: unknown) => {
93
+ const parsed =
94
+ requestInput instanceof URL
95
+ ? requestInput
96
+ : new URL(
97
+ typeof requestInput === 'string'
98
+ ? requestInput
99
+ : requestInput instanceof Request
100
+ ? requestInput.url
101
+ : String(requestInput)
102
+ );
103
+
104
+ return parsed.pathname.includes('/v1/responses') || parsed.pathname.includes('/chat/completions')
105
+ ? new URL(CODEX_API_ENDPOINT)
106
+ : parsed;
107
+ };
108
+
109
+ const normalizeBody = (body: unknown): string | undefined => {
110
+ if (typeof body === 'string') return body;
111
+ if (body instanceof Uint8Array) return new TextDecoder().decode(body);
112
+ if (body instanceof ArrayBuffer) return new TextDecoder().decode(new Uint8Array(body));
113
+ return undefined;
114
+ };
115
+
116
+ const sanitizeCodexPayload = (parsed: Record<string, unknown>) => {
117
+ parsed.store = false;
118
+ if ('previous_response_id' in parsed) {
119
+ delete parsed.previous_response_id;
120
+ }
121
+ const input = parsed.input;
122
+ if (!Array.isArray(input)) return;
123
+ parsed.input = input
124
+ .filter(
125
+ (item) =>
126
+ !(item && typeof item === 'object' && 'type' in item && item.type === 'item_reference')
127
+ )
128
+ .map((item) => {
129
+ if (!item || typeof item !== 'object') return item;
130
+ // Strip item IDs to avoid referencing non-persisted items when store=false.
131
+ const { id: _unused, ...rest } = item as Record<string, unknown>;
132
+ return rest;
133
+ });
134
+ };
135
+
136
+ const injectCodexDefaults = (
137
+ init: RequestInit | undefined,
138
+ instructions?: string
139
+ ): RequestInit | undefined => {
140
+ if (!instructions) return init;
141
+ const bodyText = normalizeBody(init?.body);
142
+ if (!bodyText) return init;
143
+
144
+ try {
145
+ const parsed = JSON.parse(bodyText) as Record<string, unknown>;
146
+ if (parsed.instructions == null) {
147
+ parsed.instructions = instructions;
148
+ }
149
+ sanitizeCodexPayload(parsed);
150
+ return { ...init, body: JSON.stringify(parsed) };
151
+ } catch {
152
+ return init;
153
+ }
154
+ };
155
+
156
+ export function createOpenAICodex(
157
+ options: {
158
+ apiKey?: string;
159
+ accountId?: string;
160
+ baseURL?: string;
161
+ headers?: Record<string, string>;
162
+ name?: string;
163
+ instructions?: string;
164
+ sessionId?: string;
165
+ } = {}
166
+ ) {
167
+ const customFetch = (async (requestInput, init) => {
168
+ const storedAuth = await Auth.getCredentials('openai');
169
+ let accessToken = options.apiKey;
170
+ let accountId = options.accountId;
171
+
172
+ if (storedAuth?.type === 'oauth') {
173
+ accessToken = storedAuth.access;
174
+ accountId = storedAuth.accountId ?? accountId;
175
+
176
+ if (!storedAuth.access || storedAuth.expires < Date.now()) {
177
+ const tokens = await refreshAccessToken(storedAuth.refresh);
178
+ const refreshedAccountId = extractAccountId(tokens) ?? accountId;
179
+ accessToken = tokens.access_token;
180
+ accountId = refreshedAccountId;
181
+ await Auth.setCredentials('openai', {
182
+ type: 'oauth',
183
+ refresh: tokens.refresh_token ?? storedAuth.refresh,
184
+ access: tokens.access_token,
185
+ expires: Date.now() + (tokens.expires_in ?? 3600) * 1000,
186
+ ...(refreshedAccountId ? { accountId: refreshedAccountId } : {})
187
+ });
188
+ }
189
+ }
190
+
191
+ const url = rewriteUrl(requestInput);
192
+ const headerSource =
193
+ init?.headers ?? (requestInput instanceof Request ? requestInput.headers : undefined);
194
+ const headers = buildHeaders(headerSource, accountId);
195
+ const fallbackInstructions =
196
+ options.instructions ?? 'You are btca, an expert documentation search agent.';
197
+ const nextInit = injectCodexDefaults(init, fallbackInstructions);
198
+ headers.set('originator', 'opencode');
199
+ headers.set('User-Agent', USER_AGENT);
200
+ if (options.sessionId) {
201
+ headers.set('session_id', options.sessionId);
202
+ }
203
+ if (accessToken) {
204
+ headers.set('authorization', `Bearer ${accessToken}`);
205
+ }
206
+ return fetch(url, { ...nextInit, headers });
207
+ }) as typeof fetch;
208
+
209
+ if (fetch.preconnect) {
210
+ customFetch.preconnect = fetch.preconnect.bind(fetch);
211
+ }
212
+
213
+ return createOpenAI({
214
+ apiKey: options.apiKey,
215
+ baseURL: options.baseURL,
216
+ headers: options.headers,
217
+ name: options.name,
218
+ fetch: customFetch
219
+ });
220
+ }