@ai-sdk/harness-opencode 1.0.108 → 1.0.110

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.
@@ -9,11 +9,21 @@ import {
9
9
  isHarnessAuthenticationEnvironment,
10
10
  } from '@ai-sdk/harness/utils';
11
11
 
12
+ export const OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE =
13
+ 'AI_SDK_OPENCODE_NATIVE_ACCESS_TOKEN';
14
+
12
15
  export const OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES = [
13
16
  'AI_GATEWAY_API_KEY',
14
17
  'OPENAI_API_KEY',
15
18
  'ANTHROPIC_API_KEY',
16
19
  'ANTHROPIC_AUTH_TOKEN',
20
+ 'XAI_API_KEY',
21
+ 'GITHUB_TOKEN',
22
+ 'GITHUB_COPILOT_TOKEN',
23
+ 'POE_API_KEY',
24
+ 'OPENCODE_API_KEY',
25
+ 'GITLAB_TOKEN',
26
+ OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE,
17
27
  ] as const;
18
28
 
19
29
  export function createOpenCodeRequestTransformations({
@@ -102,12 +112,55 @@ export function createOpenCodeRequestTransformations({
102
112
 
103
113
  return transformations;
104
114
  }
115
+ case 'xai':
116
+ return createBearerTransformation({
117
+ environment,
118
+ sandboxEnvironment,
119
+ environmentVariableName: 'XAI_API_KEY',
120
+ matchUrl: environment.XAI_BASE_URL ?? 'https://api.x.ai/v1',
121
+ });
122
+ case 'github-copilot':
123
+ return createBearerTransformation({
124
+ environment,
125
+ sandboxEnvironment,
126
+ environmentVariableName:
127
+ environment.GITHUB_COPILOT_TOKEN != null
128
+ ? 'GITHUB_COPILOT_TOKEN'
129
+ : 'GITHUB_TOKEN',
130
+ matchUrl: 'https://api.githubcopilot.com',
131
+ });
132
+ case 'poe':
133
+ return createBearerTransformation({
134
+ environment,
135
+ sandboxEnvironment,
136
+ environmentVariableName: 'POE_API_KEY',
137
+ matchUrl: 'https://api.poe.com',
138
+ });
139
+ case 'opencode-go':
140
+ return createBearerTransformation({
141
+ environment,
142
+ sandboxEnvironment,
143
+ environmentVariableName: 'OPENCODE_API_KEY',
144
+ matchUrl: 'https://opencode.ai/zen/go/v1',
145
+ });
146
+ case 'gitlab':
147
+ return createBearerTransformation({
148
+ environment,
149
+ sandboxEnvironment,
150
+ environmentVariableName: 'GITLAB_TOKEN',
151
+ matchUrl: environment.GITLAB_INSTANCE_URL ?? 'https://gitlab.com',
152
+ });
105
153
  }
106
154
  }
107
155
 
108
156
  export type OpenCodeResolvedAuthenticationMode =
109
157
  | 'anthropic'
110
158
  | 'openai'
159
+ | 'xai'
160
+ | 'github-copilot'
161
+ | 'poe'
162
+ | 'opencode-go'
163
+ | 'gitlab'
111
164
  | 'ai-gateway';
112
165
 
113
166
  export type OpenCodeAuthenticationMode = HarnessV1Authentication<
@@ -120,13 +173,13 @@ export function resolveOpenCodeProvider({
120
173
  }: {
121
174
  model?: string;
122
175
  provider?: string;
123
- }): 'anthropic' | 'openai' {
124
- if (provider === 'anthropic' || provider === 'openai') {
176
+ }): Exclude<OpenCodeResolvedAuthenticationMode, 'ai-gateway'> {
177
+ if (isDirectProvider(provider)) {
125
178
  return provider;
126
179
  }
127
180
  if (model?.includes('/')) {
128
181
  const [modelProvider] = model.split('/');
129
- if (modelProvider === 'anthropic' || modelProvider === 'openai') {
182
+ if (isDirectProvider(modelProvider)) {
130
183
  return modelProvider;
131
184
  }
132
185
  }
@@ -167,11 +220,14 @@ export function resolveOpenCodeEnv({
167
220
  const suppliedEnvironment = isHarnessAuthenticationEnvironment(auth);
168
221
  const authenticationEnvironment = suppliedEnvironment ? auth : processEnv;
169
222
  const selectedProvider = resolveOpenCodeProvider({ model, provider });
170
- if (selectedProvider === 'openai' && auth === 'openai') {
171
- return pickOpenAI({ processEnv: authenticationEnvironment });
172
- }
173
- if (selectedProvider === 'anthropic' && auth === 'anthropic') {
174
- return pickAnthropic({ processEnv: authenticationEnvironment });
223
+ if (
224
+ (selectedProvider === 'openai' && auth === 'openai') ||
225
+ (selectedProvider === 'anthropic' && auth === 'anthropic')
226
+ ) {
227
+ return pickDirectProvider({
228
+ provider: selectedProvider,
229
+ processEnv: authenticationEnvironment,
230
+ });
175
231
  }
176
232
 
177
233
  const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({
@@ -180,9 +236,10 @@ export function resolveOpenCodeEnv({
180
236
  if (auth === 'ai-gateway' || gatewayAuthFromEnv.apiKey) {
181
237
  return pickGateway({ gatewayAuthFromEnv });
182
238
  }
183
- return selectedProvider === 'openai'
184
- ? pickOpenAI({ processEnv: authenticationEnvironment })
185
- : pickAnthropic({ processEnv: authenticationEnvironment });
239
+ return pickDirectProvider({
240
+ provider: selectedProvider,
241
+ processEnv: authenticationEnvironment,
242
+ });
186
243
  }
187
244
 
188
245
  export function resolveOpenCodeAuthenticationMode({
@@ -249,6 +306,90 @@ function pickAnthropic({
249
306
  return env;
250
307
  }
251
308
 
309
+ function pickDirectProvider({
310
+ provider,
311
+ processEnv,
312
+ }: {
313
+ provider: Exclude<OpenCodeResolvedAuthenticationMode, 'ai-gateway'>;
314
+ processEnv: Record<string, string | undefined>;
315
+ }): Record<string, string> {
316
+ if (provider === 'openai') return pickOpenAI({ processEnv });
317
+ if (provider === 'anthropic') return pickAnthropic({ processEnv });
318
+ const names =
319
+ provider === 'xai'
320
+ ? ['XAI_API_KEY', 'XAI_BASE_URL']
321
+ : provider === 'github-copilot'
322
+ ? ['GITHUB_COPILOT_TOKEN', 'GITHUB_TOKEN']
323
+ : provider === 'poe'
324
+ ? ['POE_API_KEY']
325
+ : provider === 'opencode-go'
326
+ ? ['OPENCODE_API_KEY']
327
+ : ['GITLAB_TOKEN', 'GITLAB_INSTANCE_URL'];
328
+ return Object.fromEntries(
329
+ names.flatMap(name =>
330
+ processEnv[name] == null ? [] : [[name, processEnv[name]!]],
331
+ ),
332
+ );
333
+ }
334
+
335
+ export function hasOpenCodeCredential({
336
+ environment,
337
+ authenticationMode,
338
+ }: {
339
+ environment: Record<string, string>;
340
+ authenticationMode: OpenCodeResolvedAuthenticationMode;
341
+ }): boolean {
342
+ if (authenticationMode === 'ai-gateway') {
343
+ return environment.AI_GATEWAY_API_KEY != null;
344
+ }
345
+ return Object.keys(environment).some(
346
+ name =>
347
+ name.endsWith('_API_KEY') ||
348
+ name.endsWith('_TOKEN') ||
349
+ name === 'ANTHROPIC_AUTH_TOKEN',
350
+ );
351
+ }
352
+
353
+ function createBearerTransformation({
354
+ environment,
355
+ sandboxEnvironment,
356
+ environmentVariableName,
357
+ matchUrl,
358
+ }: {
359
+ environment: Readonly<Record<string, string>>;
360
+ sandboxEnvironment: Readonly<Record<string, string>>;
361
+ environmentVariableName: string;
362
+ matchUrl: string;
363
+ }): HarnessV1RequestTransformation[] {
364
+ const credential = environment[environmentVariableName];
365
+ const sandboxCredential = sandboxEnvironment[environmentVariableName];
366
+ return credential == null || sandboxCredential == null
367
+ ? []
368
+ : [
369
+ createCredentialRequestTransformation({
370
+ matchUrl,
371
+ matchHeaders: {
372
+ Authorization: `Bearer ${sandboxCredential}`,
373
+ },
374
+ transformHeaders: { Authorization: `Bearer ${credential}` },
375
+ }),
376
+ ];
377
+ }
378
+
379
+ function isDirectProvider(
380
+ value: string | undefined,
381
+ ): value is Exclude<OpenCodeResolvedAuthenticationMode, 'ai-gateway'> {
382
+ return (
383
+ value === 'anthropic' ||
384
+ value === 'openai' ||
385
+ value === 'xai' ||
386
+ value === 'github-copilot' ||
387
+ value === 'poe' ||
388
+ value === 'opencode-go' ||
389
+ value === 'gitlab'
390
+ );
391
+ }
392
+
252
393
  function pickGateway({
253
394
  gatewayAuthFromEnv,
254
395
  }: {
@@ -17,6 +17,7 @@ import {
17
17
  type HarnessV1PromptControl,
18
18
  type HarnessV1ResponseFormat,
19
19
  type HarnessV1PortEndpoint,
20
+ type HarnessV1RequestTransformation,
20
21
  type HarnessV1ResumeSessionState,
21
22
  type HarnessV1Session,
22
23
  type HarnessV1Skill,
@@ -59,10 +60,18 @@ import {
59
60
  import {
60
61
  createOpenCodeRequestTransformations,
61
62
  OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
62
- resolveOpenCodeAuthenticationMode,
63
- resolveOpenCodeEnv,
63
+ OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE,
64
64
  type OpenCodeAuthenticationMode,
65
65
  } from './opencode-auth';
66
+ import {
67
+ createOpenCodeGitLabSubscriptionConfig,
68
+ createOpenCodeGitLabSubscriptionRequestTransformations,
69
+ createOpenCodeSubscriptionAuthContent,
70
+ createOpenCodeSubscriptionRequestTransformations,
71
+ requestOpenCodeGitLabDirectAccess,
72
+ resolveOpenCodeAuthentication,
73
+ resolveOpenCodeGitLabSubscriptionModel,
74
+ } from './opencode-subscription';
66
75
  import {
67
76
  outboundMessageSchema,
68
77
  type InboundMessage,
@@ -298,14 +307,17 @@ export function createOpenCode(
298
307
  ? resumeData.openCodeSessionId
299
308
  : undefined;
300
309
  const coords = resumeData?.bridge;
301
- const authenticationMode = resolveOpenCodeAuthenticationMode({
302
- auth: settings.auth,
303
- provider: settings.provider,
304
- });
305
- const resolvedAuthEnvironment = resolveOpenCodeEnv({
310
+ const authentication = await resolveOpenCodeAuthentication({
306
311
  auth: settings.auth,
307
312
  provider: settings.provider,
308
313
  });
314
+ const authenticationMode = authentication.authenticationMode;
315
+ const resolvedAuthEnvironment = authentication.environment;
316
+ let resolvedOpenCodeConfig = settings.openCodeConfig;
317
+ let transformModel:
318
+ | ((model: string | undefined) => string | undefined)
319
+ | undefined;
320
+ let gitLabSubscriptionBrokered = false;
309
321
  let sandboxAuthEnvironment = resolvedAuthEnvironment;
310
322
  let sandboxCredentialEnvironment: Record<string, string> | undefined;
311
323
  let credentialsBrokered = false;
@@ -325,11 +337,58 @@ export function createOpenCode(
325
337
  ...resolvedAuthEnvironment,
326
338
  ...sandboxCredentialEnvironment,
327
339
  };
328
- const requestTransformations = createOpenCodeRequestTransformations({
340
+ const transformationSources = {
329
341
  env: resolvedAuthEnvironment,
330
342
  sandboxEnv: sandboxAuthEnvironment,
331
343
  auth: authenticationMode,
332
- });
344
+ };
345
+ const sandboxSubscriptionAccessToken =
346
+ sandboxAuthEnvironment[
347
+ OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE
348
+ ];
349
+ let requestTransformations: HarnessV1RequestTransformation[];
350
+ if (
351
+ authentication.subscription?.providerId === 'gitlab' &&
352
+ sandboxSubscriptionAccessToken != null
353
+ ) {
354
+ const directAccess = await requestOpenCodeGitLabDirectAccess({
355
+ accessToken: authentication.subscription.accessToken,
356
+ ...(authentication.subscription.enterpriseUrl == null
357
+ ? {}
358
+ : { instanceUrl: authentication.subscription.enterpriseUrl }),
359
+ ...(process.env.GITLAB_AI_GATEWAY_URL == null
360
+ ? {}
361
+ : { aiGatewayUrl: process.env.GITLAB_AI_GATEWAY_URL }),
362
+ });
363
+ requestTransformations =
364
+ createOpenCodeGitLabSubscriptionRequestTransformations({
365
+ directAccess,
366
+ sandboxAccessToken: sandboxSubscriptionAccessToken,
367
+ });
368
+ resolvedOpenCodeConfig = createOpenCodeGitLabSubscriptionConfig({
369
+ openCodeConfig: settings.openCodeConfig,
370
+ sandboxAccessToken: sandboxSubscriptionAccessToken,
371
+ aiGatewayUrl: directAccess.aiGatewayUrl,
372
+ ...(startOpts.headers == null
373
+ ? {}
374
+ : { headers: startOpts.headers }),
375
+ });
376
+ transformModel = model =>
377
+ resolveOpenCodeGitLabSubscriptionModel({
378
+ model,
379
+ provider: settings.provider,
380
+ });
381
+ gitLabSubscriptionBrokered = true;
382
+ } else {
383
+ requestTransformations =
384
+ authentication.subscription == null ||
385
+ sandboxSubscriptionAccessToken == null
386
+ ? createOpenCodeRequestTransformations(transformationSources)
387
+ : createOpenCodeSubscriptionRequestTransformations({
388
+ authentication: authentication.subscription,
389
+ sandboxAccessToken: sandboxSubscriptionAccessToken,
390
+ });
391
+ }
333
392
  if (requestTransformations.length > 0) {
334
393
  await sandboxSession.addRequestTransformations(
335
394
  requestTransformations,
@@ -398,7 +457,7 @@ export function createOpenCode(
398
457
  proc: undefined,
399
458
  provider: settings.provider,
400
459
  reasoningVariant: settings.reasoningVariant,
401
- openCodeConfig: settings.openCodeConfig,
460
+ openCodeConfig: resolvedOpenCodeConfig,
402
461
  mcpServers: settings.mcpServers,
403
462
  headers: startOpts.headers,
404
463
  openCodeSessionId: resumeSessionId,
@@ -414,6 +473,7 @@ export function createOpenCode(
414
473
  builtinToolFiltering: startOpts.builtinToolFiltering,
415
474
  sandbox: toolSafeSandboxSession,
416
475
  sandboxHomeDir,
476
+ transformModel,
417
477
  supportsUserMessageResponses: () => supportsUserMessageResponses,
418
478
  });
419
479
  } catch {}
@@ -458,8 +518,28 @@ export function createOpenCode(
458
518
  OPENCODE_CREDENTIAL_ENVIRONMENT_VARIABLES,
459
519
  });
460
520
  }
521
+ const subscriptionAccessToken =
522
+ forwardedAuthEnvironment[
523
+ OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE
524
+ ];
525
+ const forwardableAuthEnvironment = Object.fromEntries(
526
+ Object.entries(forwardedAuthEnvironment).filter(
527
+ ([name]) =>
528
+ name !== OPENCODE_SUBSCRIPTION_ACCESS_TOKEN_ENVIRONMENT_VARIABLE,
529
+ ),
530
+ );
461
531
  const env = {
462
- ...forwardedAuthEnvironment,
532
+ ...forwardableAuthEnvironment,
533
+ ...(gitLabSubscriptionBrokered ||
534
+ authentication.subscription == null ||
535
+ subscriptionAccessToken == null
536
+ ? {}
537
+ : {
538
+ OPENCODE_AUTH_CONTENT: createOpenCodeSubscriptionAuthContent({
539
+ authentication: authentication.subscription,
540
+ accessToken: subscriptionAccessToken,
541
+ }),
542
+ }),
463
543
  AI_SDK_HARNESS_CLIENT_APP: OPENCODE_CLIENT_APP,
464
544
  BRIDGE_CHANNEL_TOKEN: token,
465
545
  BRIDGE_WS_PORT: String(port),
@@ -555,7 +635,7 @@ export function createOpenCode(
555
635
  proc,
556
636
  provider: settings.provider,
557
637
  reasoningVariant: settings.reasoningVariant,
558
- openCodeConfig: settings.openCodeConfig,
638
+ openCodeConfig: resolvedOpenCodeConfig,
559
639
  mcpServers: settings.mcpServers,
560
640
  headers: startOpts.headers,
561
641
  openCodeSessionId: resumeSessionId,
@@ -571,6 +651,7 @@ export function createOpenCode(
571
651
  builtinToolFiltering: startOpts.builtinToolFiltering,
572
652
  sandbox: toolSafeSandboxSession,
573
653
  sandboxHomeDir,
654
+ transformModel,
574
655
  supportsUserMessageResponses: () => supportsUserMessageResponses,
575
656
  });
576
657
  },
@@ -764,6 +845,7 @@ function createSession({
764
845
  builtinToolFiltering,
765
846
  sandbox,
766
847
  sandboxHomeDir,
848
+ transformModel,
767
849
  supportsUserMessageResponses,
768
850
  }: {
769
851
  sessionId: string;
@@ -787,6 +869,9 @@ function createSession({
787
869
  builtinToolFiltering: HarnessV1BuiltinToolFiltering | undefined;
788
870
  sandbox: SandboxSession;
789
871
  sandboxHomeDir: string;
872
+ transformModel:
873
+ | ((model: string | undefined) => string | undefined)
874
+ | undefined;
790
875
  supportsUserMessageResponses: () => boolean;
791
876
  }): HarnessV1Session {
792
877
  let stopped = false;
@@ -950,7 +1035,7 @@ function createSession({
950
1035
  };
951
1036
 
952
1037
  const startBase = (turnModel: string | undefined) => ({
953
- model: turnModel,
1038
+ model: transformModel == null ? turnModel : transformModel(turnModel),
954
1039
  provider,
955
1040
  ...(reasoningVariant ? { variant: reasoningVariant } : {}),
956
1041
  ...(openCodeConfig == null ? {} : { openCodeConfig }),
@@ -1075,7 +1160,10 @@ function createSession({
1075
1160
  }
1076
1161
  await runCompactOperation({
1077
1162
  channel,
1078
- model: selectedModel,
1163
+ model:
1164
+ transformModel == null
1165
+ ? selectedModel
1166
+ : transformModel(selectedModel),
1079
1167
  provider,
1080
1168
  permissionMode,
1081
1169
  debug,