gitlab-ai-provider 6.9.2 → 6.9.3

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/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
4
4
 
5
+ ## <small>6.9.3 (2026-06-11)</small>
6
+
7
+ - Merge branch 'fix/concurrent-auth-token-race' into 'main' ([7306083](https://gitlab.com/vglafirov/gitlab-ai-provider/commit/7306083))
8
+ - fix(auth): prevent concurrent auth failures from token races ([49ed5c7](https://gitlab.com/vglafirov/gitlab-ai-provider/commit/49ed5c7))
9
+
5
10
  ## <small>6.9.2 (2026-06-10)</small>
6
11
 
7
12
  - Merge branch 'fix/auth-json-api-token-handling' into 'main' ([19a6e58](https://gitlab.com/vglafirov/gitlab-ai-provider/commit/19a6e58))
package/dist/index.d.mts CHANGED
@@ -6,6 +6,11 @@ interface GitLabAnthropicConfig {
6
6
  instanceUrl: string;
7
7
  getHeaders: () => Record<string, string>;
8
8
  fetch?: typeof fetch;
9
+ /**
10
+ * Optional async hook that resolves once credentials are loaded, awaited
11
+ * before the synchronous getHeaders() is used for the direct-access request.
12
+ */
13
+ ensureApiKey?: () => Promise<void>;
9
14
  /**
10
15
  * Optional callback to refresh the API key when a 401 error occurs.
11
16
  * Should clear cached credentials and re-fetch from auth provider.
@@ -1073,6 +1078,7 @@ interface GitLabOpenAIConfig {
1073
1078
  instanceUrl: string;
1074
1079
  getHeaders: () => Record<string, string>;
1075
1080
  fetch?: typeof fetch;
1081
+ ensureApiKey?: () => Promise<void>;
1076
1082
  refreshApiKey?: () => Promise<void>;
1077
1083
  openaiModel?: string;
1078
1084
  maxTokens?: number;
@@ -1431,6 +1437,14 @@ declare const DEFAULT_AI_GATEWAY_URL = "https://cloud.gitlab.com";
1431
1437
  interface GitLabDirectAccessConfig {
1432
1438
  instanceUrl: string;
1433
1439
  getHeaders: () => Record<string, string>;
1440
+ /**
1441
+ * Optional async hook that resolves once credentials are loaded.
1442
+ * `getHeaders` is synchronous, but the API key is loaded asynchronously; the
1443
+ * client awaits this before building request headers so the first request on
1444
+ * a cold provider doesn't race the lazy key load (which would otherwise throw
1445
+ * "API key is missing" or send an empty Authorization header).
1446
+ */
1447
+ ensureApiKey?: () => Promise<void>;
1434
1448
  fetch?: typeof fetch;
1435
1449
  /**
1436
1450
  * Optional callback to refresh the API key when a 401 error occurs.
package/dist/index.d.ts CHANGED
@@ -6,6 +6,11 @@ interface GitLabAnthropicConfig {
6
6
  instanceUrl: string;
7
7
  getHeaders: () => Record<string, string>;
8
8
  fetch?: typeof fetch;
9
+ /**
10
+ * Optional async hook that resolves once credentials are loaded, awaited
11
+ * before the synchronous getHeaders() is used for the direct-access request.
12
+ */
13
+ ensureApiKey?: () => Promise<void>;
9
14
  /**
10
15
  * Optional callback to refresh the API key when a 401 error occurs.
11
16
  * Should clear cached credentials and re-fetch from auth provider.
@@ -1073,6 +1078,7 @@ interface GitLabOpenAIConfig {
1073
1078
  instanceUrl: string;
1074
1079
  getHeaders: () => Record<string, string>;
1075
1080
  fetch?: typeof fetch;
1081
+ ensureApiKey?: () => Promise<void>;
1076
1082
  refreshApiKey?: () => Promise<void>;
1077
1083
  openaiModel?: string;
1078
1084
  maxTokens?: number;
@@ -1431,6 +1437,14 @@ declare const DEFAULT_AI_GATEWAY_URL = "https://cloud.gitlab.com";
1431
1437
  interface GitLabDirectAccessConfig {
1432
1438
  instanceUrl: string;
1433
1439
  getHeaders: () => Record<string, string>;
1440
+ /**
1441
+ * Optional async hook that resolves once credentials are loaded.
1442
+ * `getHeaders` is synchronous, but the API key is loaded asynchronously; the
1443
+ * client awaits this before building request headers so the first request on
1444
+ * a cold provider doesn't race the lazy key load (which would otherwise throw
1445
+ * "API key is missing" or send an empty Authorization header).
1446
+ */
1447
+ ensureApiKey?: () => Promise<void>;
1434
1448
  fetch?: typeof fetch;
1435
1449
  /**
1436
1450
  * Optional callback to refresh the API key when a 401 error occurs.
package/dist/index.js CHANGED
@@ -727,6 +727,9 @@ var GitLabDirectAccessClient = class {
727
727
  if (this.config.featureFlags && Object.keys(this.config.featureFlags).length > 0) {
728
728
  requestBody.feature_flags = this.config.featureFlags;
729
729
  }
730
+ if (this.config.ensureApiKey) {
731
+ await this.config.ensureApiKey();
732
+ }
730
733
  try {
731
734
  const response = await this.fetchFn(url, {
732
735
  method: "POST",
@@ -822,6 +825,7 @@ var GitLabAnthropicLanguageModel = class {
822
825
  this.directAccessClient = new GitLabDirectAccessClient({
823
826
  instanceUrl: config.instanceUrl,
824
827
  getHeaders: config.getHeaders,
828
+ ensureApiKey: config.ensureApiKey,
825
829
  refreshApiKey: config.refreshApiKey,
826
830
  fetch: config.fetch,
827
831
  featureFlags: config.featureFlags,
@@ -1555,6 +1559,7 @@ var GitLabOpenAILanguageModel = class {
1555
1559
  this.directAccessClient = new GitLabDirectAccessClient({
1556
1560
  instanceUrl: config.instanceUrl,
1557
1561
  getHeaders: config.getHeaders,
1562
+ ensureApiKey: config.ensureApiKey,
1558
1563
  refreshApiKey: config.refreshApiKey,
1559
1564
  fetch: config.fetch,
1560
1565
  featureFlags: config.featureFlags,
@@ -2496,7 +2501,7 @@ var import_node_async_hooks = require("async_hooks");
2496
2501
  var import_isomorphic_ws = __toESM(require("isomorphic-ws"));
2497
2502
 
2498
2503
  // src/version.ts
2499
- var VERSION = true ? "6.9.1" : "0.0.0-dev";
2504
+ var VERSION = true ? "6.9.2" : "0.0.0-dev";
2500
2505
 
2501
2506
  // src/gitlab-workflow-client.ts
2502
2507
  var WS_CONNECT_TIMEOUT_MS = 3e4;
@@ -4833,9 +4838,10 @@ var TOKEN_EXPIRY_SKEW_MS = 5 * 60 * 1e3;
4833
4838
  var OAUTH_SCOPES = ["api"];
4834
4839
 
4835
4840
  // src/gitlab-oauth-manager.ts
4841
+ var globalInFlightRefreshes = /* @__PURE__ */ new Map();
4836
4842
  var GitLabOAuthManager = class {
4837
4843
  fetch;
4838
- inFlightRefreshes = /* @__PURE__ */ new Map();
4844
+ inFlightRefreshes = globalInFlightRefreshes;
4839
4845
  constructor(fetchImpl = fetch) {
4840
4846
  this.fetch = fetchImpl;
4841
4847
  }
@@ -5064,6 +5070,39 @@ async function loadOpenCodeAuth(instanceUrl) {
5064
5070
  return void 0;
5065
5071
  }
5066
5072
  }
5073
+ var inFlightOAuthRefreshes = /* @__PURE__ */ new Map();
5074
+ async function refreshOAuthToken(auth, instanceUrl, clientId) {
5075
+ const existing = inFlightOAuthRefreshes.get(instanceUrl);
5076
+ if (existing) {
5077
+ return existing;
5078
+ }
5079
+ const refreshPromise = (async () => {
5080
+ const oauthManager = new GitLabOAuthManager();
5081
+ const refreshed = await oauthManager.exchangeRefreshToken({
5082
+ instanceUrl,
5083
+ refreshToken: auth.refresh,
5084
+ clientId
5085
+ });
5086
+ const authPath = getOpenCodeAuthPath();
5087
+ const authData = JSON.parse(fs3.readFileSync(authPath, "utf-8"));
5088
+ authData.gitlab = {
5089
+ type: "oauth",
5090
+ refresh: refreshed.refreshToken,
5091
+ access: refreshed.accessToken,
5092
+ expires: refreshed.expiresAt,
5093
+ enterpriseUrl: instanceUrl
5094
+ // Use enterpriseUrl to match auth plugin format
5095
+ };
5096
+ fs3.writeFileSync(authPath, JSON.stringify(authData, null, 2), { mode: 384 });
5097
+ return refreshed.accessToken;
5098
+ })();
5099
+ inFlightOAuthRefreshes.set(instanceUrl, refreshPromise);
5100
+ try {
5101
+ return await refreshPromise;
5102
+ } finally {
5103
+ inFlightOAuthRefreshes.delete(instanceUrl);
5104
+ }
5105
+ }
5067
5106
  async function loadApiKey(options, instanceUrl, clientId) {
5068
5107
  if (options.apiKey) {
5069
5108
  return options.apiKey;
@@ -5076,23 +5115,7 @@ async function loadApiKey(options, instanceUrl, clientId) {
5076
5115
  const oauthManager = new GitLabOAuthManager();
5077
5116
  if (oauthManager.needsRefresh(auth.expires)) {
5078
5117
  try {
5079
- const refreshed = await oauthManager.exchangeRefreshToken({
5080
- instanceUrl,
5081
- refreshToken: auth.refresh,
5082
- clientId
5083
- });
5084
- const authPath = getOpenCodeAuthPath();
5085
- const authData = JSON.parse(fs3.readFileSync(authPath, "utf-8"));
5086
- authData.gitlab = {
5087
- type: "oauth",
5088
- refresh: refreshed.refreshToken,
5089
- access: refreshed.accessToken,
5090
- expires: refreshed.expiresAt,
5091
- enterpriseUrl: instanceUrl
5092
- // Use enterpriseUrl to match auth plugin format
5093
- };
5094
- fs3.writeFileSync(authPath, JSON.stringify(authData, null, 2), { mode: 384 });
5095
- return refreshed.accessToken;
5118
+ return await refreshOAuthToken(auth, instanceUrl, clientId);
5096
5119
  } catch (error) {
5097
5120
  const refreshErrorMsg = error instanceof Error ? error.message : String(error);
5098
5121
  const envApiKey = process.env[options.environmentVariableName];
@@ -5191,6 +5214,12 @@ function createGitLab(options = {}) {
5191
5214
  `ai-sdk-gitlab/${VERSION}`
5192
5215
  );
5193
5216
  };
5217
+ const ensureApiKey = async () => {
5218
+ try {
5219
+ await getApiKey();
5220
+ } catch {
5221
+ }
5222
+ };
5194
5223
  getApiKey().catch(() => {
5195
5224
  });
5196
5225
  const createAgenticChatModel = (modelId, agenticOptions) => {
@@ -5225,6 +5254,7 @@ function createGitLab(options = {}) {
5225
5254
  provider: `${providerName}.agentic`,
5226
5255
  instanceUrl,
5227
5256
  getHeaders,
5257
+ ensureApiKey,
5228
5258
  refreshApiKey,
5229
5259
  fetch: options.fetch,
5230
5260
  maxTokens: agenticOptions?.maxTokens,