@ai-sdk/harness-opencode 1.0.109 → 1.0.111

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.
@@ -0,0 +1,727 @@
1
+ import { chmod, mkdir, readFile, rename, writeFile } from 'node:fs/promises';
2
+ import { homedir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import {
5
+ HarnessCapabilityUnsupportedError,
6
+ type HarnessV1RequestTransformation,
7
+ } from '@ai-sdk/harness';
8
+ import {
9
+ createCredentialRequestTransformation,
10
+ isAccessTokenExpiringSoon,
11
+ isHarnessAuthenticationEnvironment,
12
+ isWindows,
13
+ parseJwtPayload,
14
+ refreshOAuthAccessToken,
15
+ } from '@ai-sdk/harness/utils';
16
+ import { isRecord, safeParseJSON } from '@ai-sdk/provider-utils';
17
+ import {
18
+ hasOpenCodeCredential,
19
+ OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE,
20
+ resolveOpenCodeAuthenticationMode,
21
+ resolveOpenCodeEnv,
22
+ type OpenCodeAuthenticationMode,
23
+ type OpenCodeResolvedAuthenticationMode,
24
+ } from './opencode-auth';
25
+
26
+ const OPENAI_CLIENT_ID = 'app_EMoamEEZ73f0CkXaXp7hrann';
27
+ const XAI_CLIENT_ID = 'b1a00492-073a-47ea-816f-4c329264a828';
28
+ const GITLAB_CLIENT_ID =
29
+ '1d89f9fdb23ee96d4e603201f6861dab6e143c5c3c00469a018a2d94bdc03d4e';
30
+ const GITLAB_AI_GATEWAY_URL = 'https://cloud.gitlab.com';
31
+ const GITLAB_ANTHROPIC_PROVIDER_ID = 'ai-sdk-gitlab-anthropic';
32
+ const GITLAB_OPENAI_CHAT_PROVIDER_ID = 'ai-sdk-gitlab-openai-chat';
33
+ const GITLAB_OPENAI_RESPONSES_PROVIDER_ID = 'ai-sdk-gitlab-openai-responses';
34
+
35
+ type OpenCodeGitLabModel = {
36
+ readonly providerId:
37
+ | typeof GITLAB_ANTHROPIC_PROVIDER_ID
38
+ | typeof GITLAB_OPENAI_CHAT_PROVIDER_ID
39
+ | typeof GITLAB_OPENAI_RESPONSES_PROVIDER_ID
40
+ | 'workflow';
41
+ readonly modelId: string;
42
+ };
43
+
44
+ const OPEN_CODE_GITLAB_MODELS = {
45
+ 'duo-chat-fable-5': {
46
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
47
+ modelId: 'claude-fable-5',
48
+ },
49
+ 'duo-chat-opus-5': {
50
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
51
+ modelId: 'claude-opus-5',
52
+ },
53
+ 'duo-chat-opus-4-8': {
54
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
55
+ modelId: 'claude-opus-4-8',
56
+ },
57
+ 'duo-chat-opus-4-7': {
58
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
59
+ modelId: 'claude-opus-4-7',
60
+ },
61
+ 'duo-chat-opus-4-6': {
62
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
63
+ modelId: 'claude-opus-4-6',
64
+ },
65
+ 'duo-chat-sonnet-5': {
66
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
67
+ modelId: 'claude-sonnet-5',
68
+ },
69
+ 'duo-chat-sonnet-4-6': {
70
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
71
+ modelId: 'claude-sonnet-4-6',
72
+ },
73
+ 'duo-chat-opus-4-5': {
74
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
75
+ modelId: 'claude-opus-4-5-20251101',
76
+ },
77
+ 'duo-chat-sonnet-4-5': {
78
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
79
+ modelId: 'claude-sonnet-4-5-20250929',
80
+ },
81
+ 'duo-chat-haiku-4-5': {
82
+ providerId: GITLAB_ANTHROPIC_PROVIDER_ID,
83
+ modelId: 'claude-haiku-4-5-20251001',
84
+ },
85
+ 'duo-chat-gpt-5-1': {
86
+ providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
87
+ modelId: 'gpt-5.1-2025-11-13',
88
+ },
89
+ 'duo-chat-gpt-5-2': {
90
+ providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
91
+ modelId: 'gpt-5.2-2025-12-11',
92
+ },
93
+ 'duo-chat-gpt-5-4': {
94
+ providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
95
+ modelId: 'gpt-5.4-2026-03-05',
96
+ },
97
+ 'duo-chat-gpt-5-5': {
98
+ providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
99
+ modelId: 'gpt-5.5-2026-04-23',
100
+ },
101
+ 'duo-chat-gpt-5-mini': {
102
+ providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
103
+ modelId: 'gpt-5-mini-2025-08-07',
104
+ },
105
+ 'duo-chat-gpt-5-4-mini': {
106
+ providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
107
+ modelId: 'gpt-5.4-mini',
108
+ },
109
+ 'duo-chat-gpt-5-4-nano': {
110
+ providerId: GITLAB_OPENAI_CHAT_PROVIDER_ID,
111
+ modelId: 'gpt-5.4-nano',
112
+ },
113
+ 'duo-chat-gpt-5-6-sol': {
114
+ providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
115
+ modelId: 'gpt-5.6-sol',
116
+ },
117
+ 'duo-chat-gpt-5-6-terra': {
118
+ providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
119
+ modelId: 'gpt-5.6-terra',
120
+ },
121
+ 'duo-chat-gpt-5-6-luna': {
122
+ providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
123
+ modelId: 'gpt-5.6-luna',
124
+ },
125
+ 'duo-chat-gpt-5-codex': {
126
+ providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
127
+ modelId: 'gpt-5-codex',
128
+ },
129
+ 'duo-chat-gpt-5-2-codex': {
130
+ providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
131
+ modelId: 'gpt-5.2-codex',
132
+ },
133
+ 'duo-chat-gpt-5-3-codex': {
134
+ providerId: GITLAB_OPENAI_RESPONSES_PROVIDER_ID,
135
+ modelId: 'gpt-5.3-codex',
136
+ },
137
+ 'duo-workflow': { providerId: 'workflow', modelId: 'default' },
138
+ 'duo-workflow-default': { providerId: 'workflow', modelId: 'default' },
139
+ 'duo-workflow-sonnet-4-5': {
140
+ providerId: 'workflow',
141
+ modelId: 'anthropic/claude-sonnet-4-5-20250929',
142
+ },
143
+ 'duo-workflow-sonnet-5': {
144
+ providerId: 'workflow',
145
+ modelId: 'claude_sonnet_5',
146
+ },
147
+ 'duo-workflow-opus-5': {
148
+ providerId: 'workflow',
149
+ modelId: 'claude_opus_5',
150
+ },
151
+ 'duo-workflow-sonnet-4-6': {
152
+ providerId: 'workflow',
153
+ modelId: 'claude_sonnet_4_6',
154
+ },
155
+ 'duo-workflow-opus-4-5': {
156
+ providerId: 'workflow',
157
+ modelId: 'anthropic/claude-opus-4-5-20251101',
158
+ },
159
+ 'duo-workflow-haiku-4-5': {
160
+ providerId: 'workflow',
161
+ modelId: 'claude_haiku_4_5_20251001',
162
+ },
163
+ 'duo-workflow-opus-4-6': {
164
+ providerId: 'workflow',
165
+ modelId: 'claude_opus_4_6_20260205',
166
+ },
167
+ } as const satisfies Record<string, OpenCodeGitLabModel>;
168
+
169
+ export type OpenCodeSubscriptionProvider =
170
+ | 'openai'
171
+ | 'xai'
172
+ | 'github-copilot'
173
+ | 'poe'
174
+ | 'opencode-go'
175
+ | 'gitlab';
176
+
177
+ export type OpenCodeSubscription = {
178
+ readonly providerId: OpenCodeSubscriptionProvider;
179
+ readonly accessToken: string;
180
+ readonly accountId?: string;
181
+ readonly enterpriseUrl?: string;
182
+ };
183
+
184
+ export type OpenCodeGitLabDirectAccess = {
185
+ readonly accessToken: string;
186
+ readonly headers: Readonly<Record<string, string>>;
187
+ readonly aiGatewayUrl: string;
188
+ };
189
+
190
+ export async function resolveOpenCodeAuthentication({
191
+ auth,
192
+ model,
193
+ provider,
194
+ processEnv = process.env,
195
+ readSubscription = readOpenCodeSubscription,
196
+ }: {
197
+ auth: OpenCodeAuthenticationMode | undefined;
198
+ model?: string;
199
+ provider?: string;
200
+ processEnv?: Record<string, string | undefined>;
201
+ readSubscription?: typeof readOpenCodeSubscription;
202
+ }): Promise<{
203
+ environment: Record<string, string>;
204
+ authenticationMode: OpenCodeResolvedAuthenticationMode;
205
+ subscription?: OpenCodeSubscription;
206
+ }> {
207
+ const environment = resolveOpenCodeEnv({
208
+ auth,
209
+ model,
210
+ provider,
211
+ processEnv,
212
+ });
213
+ const authenticationMode = resolveOpenCodeAuthenticationMode({
214
+ auth,
215
+ model,
216
+ provider,
217
+ processEnv,
218
+ });
219
+ if (
220
+ isHarnessAuthenticationEnvironment(auth) ||
221
+ auth === 'ai-gateway' ||
222
+ authenticationMode === 'ai-gateway' ||
223
+ hasOpenCodeCredential({ environment, authenticationMode })
224
+ ) {
225
+ return { environment, authenticationMode };
226
+ }
227
+ const subscription = await readSubscription({
228
+ providerId: authenticationMode,
229
+ env: processEnv,
230
+ });
231
+ return subscription == null
232
+ ? { environment, authenticationMode }
233
+ : {
234
+ environment: {
235
+ ...environment,
236
+ [OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE]:
237
+ subscription.accessToken,
238
+ },
239
+ authenticationMode,
240
+ subscription,
241
+ };
242
+ }
243
+
244
+ export async function readOpenCodeSubscription({
245
+ providerId,
246
+ env = process.env,
247
+ homeDirectory = homedir(),
248
+ platform = process.platform,
249
+ fetch,
250
+ }: {
251
+ providerId: string;
252
+ env?: Readonly<Record<string, string | undefined>>;
253
+ homeDirectory?: string;
254
+ platform?: NodeJS.Platform;
255
+ fetch?: typeof globalThis.fetch;
256
+ }): Promise<OpenCodeSubscription | undefined> {
257
+ if (!isSubscriptionProvider(providerId)) return undefined;
258
+ const stored = await readStore({ env, homeDirectory, platform });
259
+ if (stored == null) return undefined;
260
+ const candidate = stored.value[providerId];
261
+ if (!isRecord(candidate)) return undefined;
262
+
263
+ if (candidate.type === 'api') {
264
+ return typeof candidate.key === 'string' && candidate.key.length > 0
265
+ ? { providerId, accessToken: candidate.key }
266
+ : undefined;
267
+ }
268
+ if (candidate.type !== 'oauth') return undefined;
269
+ if (providerId === 'opencode-go') return undefined;
270
+
271
+ const access = candidate.access;
272
+ const refresh = candidate.refresh;
273
+ const expires = candidate.expires;
274
+ if (
275
+ typeof access !== 'string' ||
276
+ typeof refresh !== 'string' ||
277
+ typeof expires !== 'number'
278
+ ) {
279
+ return undefined;
280
+ }
281
+
282
+ if (providerId === 'github-copilot') {
283
+ return {
284
+ providerId,
285
+ accessToken: refresh,
286
+ ...(typeof candidate.enterpriseUrl === 'string'
287
+ ? { enterpriseUrl: candidate.enterpriseUrl }
288
+ : {}),
289
+ };
290
+ }
291
+ if (providerId === 'poe') {
292
+ if (isAccessTokenExpiringSoon({ expiresAt: expires })) {
293
+ throw new Error(
294
+ 'OpenCode Poe subscription API key is expiring soon. Run OpenCode login again.',
295
+ );
296
+ }
297
+ return { providerId, accessToken: access };
298
+ }
299
+
300
+ let accessToken = access;
301
+ let accountId =
302
+ typeof candidate.accountId === 'string'
303
+ ? candidate.accountId
304
+ : providerId === 'openai'
305
+ ? await extractOpenAIAccountId(access)
306
+ : undefined;
307
+ if (isAccessTokenExpiringSoon({ expiresAt: expires })) {
308
+ const refreshSettings = resolveRefreshSettings({
309
+ providerId,
310
+ candidate,
311
+ env,
312
+ });
313
+ const refreshed = await refreshOAuthAccessToken({
314
+ ...refreshSettings,
315
+ refreshToken: refresh,
316
+ ...(fetch == null ? {} : { fetch }),
317
+ });
318
+ accessToken = refreshed.accessToken;
319
+ if (providerId === 'openai') {
320
+ accountId =
321
+ (await extractOpenAIAccountId(refreshed.accessToken)) ?? accountId;
322
+ }
323
+ stored.value[providerId] = {
324
+ ...candidate,
325
+ access: refreshed.accessToken,
326
+ refresh: refreshed.refreshToken ?? refresh,
327
+ expires: refreshed.expiresAt,
328
+ ...(accountId == null ? {} : { accountId }),
329
+ };
330
+ await stored.write?.(stored.value);
331
+ }
332
+
333
+ return {
334
+ providerId,
335
+ accessToken,
336
+ ...(accountId == null ? {} : { accountId }),
337
+ ...(typeof candidate.enterpriseUrl === 'string'
338
+ ? { enterpriseUrl: candidate.enterpriseUrl }
339
+ : {}),
340
+ };
341
+ }
342
+
343
+ export async function requestOpenCodeGitLabDirectAccess({
344
+ accessToken,
345
+ instanceUrl = 'https://gitlab.com',
346
+ aiGatewayUrl = GITLAB_AI_GATEWAY_URL,
347
+ fetch: fetchImplementation = globalThis.fetch,
348
+ }: {
349
+ accessToken: string;
350
+ instanceUrl?: string;
351
+ aiGatewayUrl?: string;
352
+ fetch?: typeof globalThis.fetch;
353
+ }): Promise<OpenCodeGitLabDirectAccess> {
354
+ const response = await fetchImplementation(
355
+ `${instanceUrl.replace(/\/+$/, '')}/api/v4/ai/third_party_agents/direct_access`,
356
+ {
357
+ method: 'POST',
358
+ headers: {
359
+ Authorization: `Bearer ${accessToken}`,
360
+ 'Content-Type': 'application/json',
361
+ },
362
+ body: JSON.stringify({
363
+ feature_flags: { DuoAgentPlatformNext: true },
364
+ }),
365
+ },
366
+ );
367
+ if (!response.ok) {
368
+ throw new Error(
369
+ `OpenCode GitLab direct access request failed with status ${response.status}.`,
370
+ );
371
+ }
372
+
373
+ const parsed = await safeParseJSON({ text: await response.text() });
374
+ if (!parsed.success || !isRecord(parsed.value)) {
375
+ throw new Error('OpenCode GitLab direct access returned invalid JSON.');
376
+ }
377
+ const token = parsed.value.token;
378
+ const headers = parsed.value.headers;
379
+ if (
380
+ typeof token !== 'string' ||
381
+ token.length === 0 ||
382
+ !isRecord(headers) ||
383
+ Object.values(headers).some(value => typeof value !== 'string')
384
+ ) {
385
+ throw new Error(
386
+ 'OpenCode GitLab direct access returned invalid credentials.',
387
+ );
388
+ }
389
+
390
+ return {
391
+ accessToken: token,
392
+ headers: headers as Record<string, string>,
393
+ aiGatewayUrl: aiGatewayUrl.replace(/\/+$/, ''),
394
+ };
395
+ }
396
+
397
+ export function createOpenCodeGitLabSubscriptionRequestTransformations({
398
+ directAccess,
399
+ sandboxAccessToken,
400
+ }: {
401
+ directAccess: OpenCodeGitLabDirectAccess;
402
+ sandboxAccessToken: string;
403
+ }): HarnessV1RequestTransformation[] {
404
+ const headers = Object.fromEntries(
405
+ Object.entries(directAccess.headers).filter(
406
+ ([name]) =>
407
+ name.toLowerCase() !== 'authorization' &&
408
+ name.toLowerCase() !== 'x-api-key',
409
+ ),
410
+ );
411
+ const transformHeaders = {
412
+ ...headers,
413
+ Authorization: `Bearer ${directAccess.accessToken}`,
414
+ };
415
+ return [
416
+ `${directAccess.aiGatewayUrl}/ai/v1/proxy/anthropic`,
417
+ `${directAccess.aiGatewayUrl}/ai/v1/proxy/openai/v1`,
418
+ ].map(matchUrl =>
419
+ createCredentialRequestTransformation({
420
+ matchUrl,
421
+ matchHeaders: {
422
+ Authorization: `Bearer ${sandboxAccessToken}`,
423
+ },
424
+ transformHeaders,
425
+ }),
426
+ );
427
+ }
428
+
429
+ export function createOpenCodeGitLabSubscriptionConfig({
430
+ openCodeConfig,
431
+ sandboxAccessToken,
432
+ aiGatewayUrl,
433
+ headers,
434
+ }: {
435
+ openCodeConfig: Record<string, unknown> | undefined;
436
+ sandboxAccessToken: string;
437
+ aiGatewayUrl: string;
438
+ headers?: Readonly<Record<string, string>>;
439
+ }): Record<string, unknown> {
440
+ const providerOptions = headers == null ? {} : { headers };
441
+ const providers = {
442
+ [GITLAB_ANTHROPIC_PROVIDER_ID]: {
443
+ name: 'GitLab Anthropic',
444
+ npm: '@ai-sdk/anthropic',
445
+ api: `${aiGatewayUrl}/ai/v1/proxy/anthropic/v1`,
446
+ options: { authToken: sandboxAccessToken, ...providerOptions },
447
+ models: createOpenCodeGitLabModels(GITLAB_ANTHROPIC_PROVIDER_ID),
448
+ },
449
+ [GITLAB_OPENAI_CHAT_PROVIDER_ID]: {
450
+ name: 'GitLab OpenAI Chat',
451
+ npm: '@ai-sdk/openai-compatible',
452
+ api: `${aiGatewayUrl}/ai/v1/proxy/openai/v1`,
453
+ options: { apiKey: sandboxAccessToken, ...providerOptions },
454
+ models: createOpenCodeGitLabModels(GITLAB_OPENAI_CHAT_PROVIDER_ID),
455
+ },
456
+ [GITLAB_OPENAI_RESPONSES_PROVIDER_ID]: {
457
+ name: 'GitLab OpenAI Responses',
458
+ npm: '@ai-sdk/openai',
459
+ api: `${aiGatewayUrl}/ai/v1/proxy/openai/v1`,
460
+ options: { apiKey: sandboxAccessToken, ...providerOptions },
461
+ models: createOpenCodeGitLabModels(GITLAB_OPENAI_RESPONSES_PROVIDER_ID),
462
+ },
463
+ };
464
+ const configuredProviders = isRecord(openCodeConfig?.provider)
465
+ ? openCodeConfig.provider
466
+ : {};
467
+ return {
468
+ ...openCodeConfig,
469
+ provider: { ...configuredProviders, ...providers },
470
+ };
471
+ }
472
+
473
+ export function resolveOpenCodeGitLabSubscriptionModel({
474
+ model,
475
+ provider,
476
+ }: {
477
+ model: string | undefined;
478
+ provider: string | undefined;
479
+ }): string | undefined {
480
+ if (model == null) {
481
+ if (provider === 'gitlab') {
482
+ throw new HarnessCapabilityUnsupportedError({
483
+ harnessId: 'opencode',
484
+ message:
485
+ "Harness 'opencode' requires an explicit GitLab model when native GitLab subscription credentials are brokered.",
486
+ });
487
+ }
488
+ return undefined;
489
+ }
490
+ const modelProvider = model.includes('/') ? model.split('/')[0] : provider;
491
+ if (modelProvider !== 'gitlab') return model;
492
+ const modelId = model.includes('/')
493
+ ? model.slice(model.indexOf('/') + 1)
494
+ : model;
495
+ const mapping: OpenCodeGitLabModel | undefined =
496
+ OPEN_CODE_GITLAB_MODELS[modelId as keyof typeof OPEN_CODE_GITLAB_MODELS];
497
+ if (mapping == null) {
498
+ throw new HarnessCapabilityUnsupportedError({
499
+ harnessId: 'opencode',
500
+ message: `Harness 'opencode' cannot broker native GitLab subscription credentials for unknown model '${modelId}'.`,
501
+ });
502
+ }
503
+ if (mapping.providerId === 'workflow') {
504
+ throw new HarnessCapabilityUnsupportedError({
505
+ harnessId: 'opencode',
506
+ message: `Harness 'opencode' cannot broker native GitLab subscription credentials for workflow model '${modelId}' because it authenticates over WebSocket. Use a sandbox without credential brokering to use the credential-forwarding fallback.`,
507
+ });
508
+ }
509
+ return `${mapping.providerId}/${modelId}`;
510
+ }
511
+
512
+ function createOpenCodeGitLabModels(
513
+ providerId: Exclude<OpenCodeGitLabModel['providerId'], 'workflow'>,
514
+ ): Record<string, { id: string; name: string }> {
515
+ return Object.fromEntries(
516
+ Object.entries(OPEN_CODE_GITLAB_MODELS).flatMap(([id, model]) =>
517
+ model.providerId === providerId
518
+ ? [[id, { id: model.modelId, name: id }]]
519
+ : [],
520
+ ),
521
+ );
522
+ }
523
+
524
+ export function createOpenCodeSubscriptionAuthContent({
525
+ authentication,
526
+ accessToken,
527
+ }: {
528
+ authentication: OpenCodeSubscription;
529
+ accessToken: string;
530
+ }): string {
531
+ const { providerId } = authentication;
532
+ const value =
533
+ providerId === 'opencode-go'
534
+ ? { type: 'api', key: accessToken }
535
+ : {
536
+ type: 'oauth',
537
+ access: accessToken,
538
+ refresh:
539
+ providerId === 'github-copilot' ? accessToken : 'host-managed',
540
+ expires: Number.MAX_SAFE_INTEGER,
541
+ ...(authentication.accountId == null
542
+ ? {}
543
+ : { accountId: authentication.accountId }),
544
+ ...(authentication.enterpriseUrl == null
545
+ ? {}
546
+ : { enterpriseUrl: authentication.enterpriseUrl }),
547
+ };
548
+ return JSON.stringify({ [providerId]: value });
549
+ }
550
+
551
+ export function createOpenCodeSubscriptionRequestTransformations({
552
+ authentication,
553
+ sandboxAccessToken,
554
+ }: {
555
+ authentication: OpenCodeSubscription;
556
+ sandboxAccessToken: string;
557
+ }): HarnessV1RequestTransformation[] {
558
+ const matchUrl = resolveRequestUrl(authentication);
559
+ const transformHeaders = {
560
+ Authorization: `Bearer ${authentication.accessToken}`,
561
+ ...(authentication.providerId === 'openai' && authentication.accountId
562
+ ? { 'ChatGPT-Account-Id': authentication.accountId }
563
+ : {}),
564
+ };
565
+ const transformations = [
566
+ createCredentialRequestTransformation({
567
+ matchUrl,
568
+ matchHeaders: {
569
+ Authorization: `Bearer ${sandboxAccessToken}`,
570
+ },
571
+ transformHeaders,
572
+ }),
573
+ ];
574
+ if (
575
+ authentication.providerId === 'poe' ||
576
+ authentication.providerId === 'opencode-go'
577
+ ) {
578
+ transformations.push(
579
+ createCredentialRequestTransformation({
580
+ matchUrl,
581
+ matchHeaders: { 'x-api-key': sandboxAccessToken },
582
+ transformHeaders: { 'x-api-key': authentication.accessToken },
583
+ }),
584
+ );
585
+ }
586
+ return transformations;
587
+ }
588
+
589
+ function resolveRefreshSettings({
590
+ providerId,
591
+ candidate,
592
+ env,
593
+ }: {
594
+ providerId: Exclude<
595
+ OpenCodeSubscriptionProvider,
596
+ 'github-copilot' | 'poe' | 'opencode-go'
597
+ >;
598
+ candidate: Record<string, unknown>;
599
+ env: Readonly<Record<string, string | undefined>>;
600
+ }): {
601
+ tokenUrl: string;
602
+ clientId: string;
603
+ } {
604
+ if (providerId === 'openai') {
605
+ return {
606
+ tokenUrl: 'https://auth.openai.com/oauth/token',
607
+ clientId: OPENAI_CLIENT_ID,
608
+ };
609
+ }
610
+ if (providerId === 'xai') {
611
+ return {
612
+ tokenUrl: 'https://auth.x.ai/oauth2/token',
613
+ clientId: XAI_CLIENT_ID,
614
+ };
615
+ }
616
+ const instanceUrl =
617
+ (typeof candidate.enterpriseUrl === 'string'
618
+ ? candidate.enterpriseUrl
619
+ : env.GITLAB_INSTANCE_URL) ?? 'https://gitlab.com';
620
+ return {
621
+ tokenUrl: `${instanceUrl.replace(/\/+$/, '')}/oauth/token`,
622
+ clientId: env.GITLAB_OAUTH_CLIENT_ID ?? GITLAB_CLIENT_ID,
623
+ };
624
+ }
625
+
626
+ function resolveRequestUrl(authentication: OpenCodeSubscription): string {
627
+ switch (authentication.providerId) {
628
+ case 'openai':
629
+ return 'https://chatgpt.com/backend-api/codex';
630
+ case 'xai':
631
+ return 'https://api.x.ai/v1';
632
+ case 'github-copilot':
633
+ return authentication.enterpriseUrl == null
634
+ ? 'https://api.githubcopilot.com'
635
+ : `https://copilot-api.${normalizeDomain(authentication.enterpriseUrl)}`;
636
+ case 'poe':
637
+ return 'https://api.poe.com';
638
+ case 'opencode-go':
639
+ return 'https://opencode.ai/zen/go/v1';
640
+ case 'gitlab':
641
+ return authentication.enterpriseUrl ?? 'https://gitlab.com';
642
+ }
643
+ }
644
+
645
+ function normalizeDomain(value: string): string {
646
+ try {
647
+ return new URL(value.includes('://') ? value : `https://${value}`).host;
648
+ } catch {
649
+ return value.replace(/^https?:\/\//, '').replace(/\/+$/, '');
650
+ }
651
+ }
652
+
653
+ async function extractOpenAIAccountId(
654
+ accessToken: string,
655
+ ): Promise<string | undefined> {
656
+ const payload = await parseJwtPayload({ token: accessToken });
657
+ const auth = payload?.['https://api.openai.com/auth'];
658
+ if (!isRecord(auth)) return undefined;
659
+ const accountId = auth.chatgpt_account_id;
660
+ return typeof accountId === 'string' ? accountId : undefined;
661
+ }
662
+
663
+ async function readStore({
664
+ env,
665
+ homeDirectory,
666
+ platform,
667
+ }: {
668
+ env: Readonly<Record<string, string | undefined>>;
669
+ homeDirectory: string;
670
+ platform: NodeJS.Platform;
671
+ }): Promise<
672
+ | {
673
+ value: Record<string, unknown>;
674
+ write?: (value: Record<string, unknown>) => Promise<void>;
675
+ }
676
+ | undefined
677
+ > {
678
+ if (env.OPENCODE_AUTH_CONTENT != null) {
679
+ const parsed = await safeParseJSON({ text: env.OPENCODE_AUTH_CONTENT });
680
+ return parsed.success && isRecord(parsed.value)
681
+ ? { value: parsed.value }
682
+ : undefined;
683
+ }
684
+ const authPath =
685
+ env.XDG_DATA_HOME != null
686
+ ? join(env.XDG_DATA_HOME, 'opencode', 'auth.json')
687
+ : isWindows(platform)
688
+ ? join(homeDirectory, '.opencode', 'auth.json')
689
+ : join(homeDirectory, '.local', 'share', 'opencode', 'auth.json');
690
+ const text = await readFile(authPath, 'utf8').catch(() => undefined);
691
+ if (text == null) return undefined;
692
+ const parsed = await safeParseJSON({ text });
693
+ if (!parsed.success || !isRecord(parsed.value)) return undefined;
694
+ return {
695
+ value: parsed.value,
696
+ write: value => writeStore({ authPath, value }),
697
+ };
698
+ }
699
+
700
+ async function writeStore({
701
+ authPath,
702
+ value,
703
+ }: {
704
+ authPath: string;
705
+ value: Record<string, unknown>;
706
+ }): Promise<void> {
707
+ await mkdir(dirname(authPath), { recursive: true });
708
+ const temporaryPath = `${authPath}.${process.pid}.tmp`;
709
+ await writeFile(temporaryPath, `${JSON.stringify(value, null, 2)}\n`, {
710
+ mode: 0o600,
711
+ });
712
+ await chmod(temporaryPath, 0o600);
713
+ await rename(temporaryPath, authPath);
714
+ }
715
+
716
+ function isSubscriptionProvider(
717
+ value: string,
718
+ ): value is OpenCodeSubscriptionProvider {
719
+ return (
720
+ value === 'openai' ||
721
+ value === 'xai' ||
722
+ value === 'github-copilot' ||
723
+ value === 'poe' ||
724
+ value === 'opencode-go' ||
725
+ value === 'gitlab'
726
+ );
727
+ }