@gitlab/gitlab-ai-provider 3.1.2 → 3.2.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.
package/dist/index.js CHANGED
@@ -32,22 +32,30 @@ __export(index_exports, {
32
32
  BUNDLED_CLIENT_ID: () => BUNDLED_CLIENT_ID,
33
33
  DEFAULT_AI_GATEWAY_URL: () => DEFAULT_AI_GATEWAY_URL,
34
34
  GITLAB_COM_URL: () => GITLAB_COM_URL,
35
- GitLabAgenticLanguageModel: () => GitLabAgenticLanguageModel,
35
+ GitLabAnthropicLanguageModel: () => GitLabAnthropicLanguageModel,
36
36
  GitLabDirectAccessClient: () => GitLabDirectAccessClient,
37
37
  GitLabError: () => GitLabError,
38
38
  GitLabOAuthManager: () => GitLabOAuthManager,
39
+ GitLabOpenAILanguageModel: () => GitLabOpenAILanguageModel,
39
40
  GitLabProjectCache: () => GitLabProjectCache,
40
41
  GitLabProjectDetector: () => GitLabProjectDetector,
41
42
  MODEL_ID_TO_ANTHROPIC_MODEL: () => MODEL_ID_TO_ANTHROPIC_MODEL,
43
+ MODEL_MAPPINGS: () => MODEL_MAPPINGS,
42
44
  OAUTH_SCOPES: () => OAUTH_SCOPES,
43
45
  TOKEN_EXPIRY_SKEW_MS: () => TOKEN_EXPIRY_SKEW_MS,
44
46
  createGitLab: () => createGitLab,
45
47
  getAnthropicModelForModelId: () => getAnthropicModelForModelId,
46
- gitlab: () => gitlab
48
+ getModelMapping: () => getModelMapping,
49
+ getOpenAIApiType: () => getOpenAIApiType,
50
+ getOpenAIModelForModelId: () => getOpenAIModelForModelId,
51
+ getProviderForModelId: () => getProviderForModelId,
52
+ getValidModelsForProvider: () => getValidModelsForProvider,
53
+ gitlab: () => gitlab,
54
+ isResponsesApiModel: () => isResponsesApiModel
47
55
  });
48
56
  module.exports = __toCommonJS(index_exports);
49
57
 
50
- // src/gitlab-agentic-language-model.ts
58
+ // src/gitlab-anthropic-language-model.ts
51
59
  var import_sdk = __toESM(require("@anthropic-ai/sdk"));
52
60
 
53
61
  // src/gitlab-direct-access.ts
@@ -182,6 +190,15 @@ var GitLabDirectAccessClient = class {
182
190
  const baseUrl = this.aiGatewayUrl.replace(/\/$/, "");
183
191
  return `${baseUrl}/ai/v1/proxy/anthropic/`;
184
192
  }
193
+ /**
194
+ * Get the OpenAI proxy base URL
195
+ * Note: The OpenAI SDK expects a base URL like https://api.openai.com/v1
196
+ * and appends paths like /chat/completions. So we need /v1 at the end.
197
+ */
198
+ getOpenAIProxyUrl() {
199
+ const baseUrl = this.aiGatewayUrl.replace(/\/$/, "");
200
+ return `${baseUrl}/ai/v1/proxy/openai/v1`;
201
+ }
185
202
  /**
186
203
  * Invalidate the cached token
187
204
  */
@@ -191,10 +208,8 @@ var GitLabDirectAccessClient = class {
191
208
  }
192
209
  };
193
210
 
194
- // src/gitlab-agentic-language-model.ts
195
- var debugLog = (..._args) => {
196
- };
197
- var GitLabAgenticLanguageModel = class {
211
+ // src/gitlab-anthropic-language-model.ts
212
+ var GitLabAnthropicLanguageModel = class {
198
213
  specificationVersion = "v2";
199
214
  modelId;
200
215
  supportedUrls = {};
@@ -222,14 +237,7 @@ var GitLabAgenticLanguageModel = class {
222
237
  */
223
238
  async getAnthropicClient(forceRefresh = false) {
224
239
  const tokenData = await this.directAccessClient.getDirectAccessToken(forceRefresh);
225
- debugLog("[gitlab-ai-provider] Token headers from GitLab:", tokenData.headers);
226
- debugLog("[gitlab-ai-provider] Proxy URL:", this.directAccessClient.getAnthropicProxyUrl());
227
240
  const { "x-api-key": _removed, ...filteredHeaders } = tokenData.headers;
228
- if (_removed) {
229
- debugLog(
230
- "[gitlab-ai-provider] Filtered out x-api-key from headers (using authToken instead)"
231
- );
232
- }
233
241
  this.anthropicClient = new import_sdk.default({
234
242
  apiKey: null,
235
243
  authToken: tokenData.token,
@@ -468,114 +476,155 @@ var GitLabAgenticLanguageModel = class {
468
476
  const self = this;
469
477
  const stream = new ReadableStream({
470
478
  start: async (controller) => {
479
+ const contentBlocks = {};
480
+ const usage = {
481
+ inputTokens: 0,
482
+ outputTokens: 0,
483
+ totalTokens: 0
484
+ };
485
+ let finishReason = "unknown";
471
486
  try {
472
- const anthropicStream = client.messages.stream(requestBody);
473
- let currentTextBlockId = null;
474
- let currentToolBlockId = null;
475
- let currentToolName = null;
476
- const usage = {
477
- inputTokens: 0,
478
- outputTokens: 0,
479
- totalTokens: 0
480
- };
481
- let finishReason = "unknown";
487
+ const anthropicStream = client.messages.stream(requestBody, {
488
+ signal: options.abortSignal
489
+ });
482
490
  controller.enqueue({
483
491
  type: "stream-start",
484
492
  warnings: []
485
493
  });
486
- for await (const event of anthropicStream) {
487
- switch (event.type) {
488
- case "message_start":
489
- if (event.message.usage) {
490
- usage.inputTokens = event.message.usage.input_tokens;
491
- }
492
- controller.enqueue({
493
- type: "response-metadata",
494
- id: event.message.id,
495
- modelId: event.message.model
496
- });
497
- break;
498
- case "content_block_start":
499
- if (event.content_block.type === "text") {
500
- currentTextBlockId = `text-${event.index}`;
501
- controller.enqueue({
502
- type: "text-start",
503
- id: currentTextBlockId
504
- });
505
- } else if (event.content_block.type === "tool_use") {
506
- currentToolBlockId = event.content_block.id;
507
- currentToolName = event.content_block.name;
508
- controller.enqueue({
509
- type: "tool-input-start",
510
- id: currentToolBlockId,
511
- toolName: currentToolName
512
- });
513
- }
514
- break;
515
- case "content_block_delta":
516
- if (event.delta.type === "text_delta" && currentTextBlockId) {
517
- controller.enqueue({
518
- type: "text-delta",
519
- id: currentTextBlockId,
520
- delta: event.delta.text
521
- });
522
- } else if (event.delta.type === "input_json_delta" && currentToolBlockId) {
523
- controller.enqueue({
524
- type: "tool-input-delta",
525
- id: currentToolBlockId,
526
- delta: event.delta.partial_json
527
- });
528
- }
529
- break;
530
- case "content_block_stop":
531
- if (currentTextBlockId) {
532
- controller.enqueue({
533
- type: "text-end",
534
- id: currentTextBlockId
535
- });
536
- currentTextBlockId = null;
537
- }
538
- if (currentToolBlockId) {
539
- controller.enqueue({
540
- type: "tool-input-end",
541
- id: currentToolBlockId
542
- });
543
- currentToolBlockId = null;
544
- currentToolName = null;
545
- }
546
- break;
547
- case "message_delta":
548
- if (event.usage) {
549
- usage.outputTokens = event.usage.output_tokens;
550
- usage.totalTokens = (usage.inputTokens || 0) + event.usage.output_tokens;
551
- }
552
- if (event.delta.stop_reason) {
553
- finishReason = self.convertFinishReason(event.delta.stop_reason);
554
- }
555
- break;
556
- case "message_stop": {
557
- const finalMessage = await anthropicStream.finalMessage();
558
- for (const block of finalMessage.content) {
559
- if (block.type === "tool_use") {
494
+ await new Promise((resolve2, reject) => {
495
+ anthropicStream.on("streamEvent", (event) => {
496
+ try {
497
+ switch (event.type) {
498
+ case "message_start":
499
+ if (event.message.usage) {
500
+ usage.inputTokens = event.message.usage.input_tokens;
501
+ }
502
+ controller.enqueue({
503
+ type: "response-metadata",
504
+ id: event.message.id,
505
+ modelId: event.message.model
506
+ });
507
+ break;
508
+ case "content_block_start":
509
+ if (event.content_block.type === "text") {
510
+ const textId = `text-${event.index}`;
511
+ contentBlocks[event.index] = { type: "text", id: textId };
512
+ controller.enqueue({
513
+ type: "text-start",
514
+ id: textId
515
+ });
516
+ } else if (event.content_block.type === "tool_use") {
517
+ contentBlocks[event.index] = {
518
+ type: "tool-call",
519
+ toolCallId: event.content_block.id,
520
+ toolName: event.content_block.name,
521
+ input: ""
522
+ };
523
+ controller.enqueue({
524
+ type: "tool-input-start",
525
+ id: event.content_block.id,
526
+ toolName: event.content_block.name
527
+ });
528
+ }
529
+ break;
530
+ case "content_block_delta": {
531
+ const block = contentBlocks[event.index];
532
+ if (event.delta.type === "text_delta" && block?.type === "text") {
533
+ controller.enqueue({
534
+ type: "text-delta",
535
+ id: block.id,
536
+ delta: event.delta.text
537
+ });
538
+ } else if (event.delta.type === "input_json_delta" && block?.type === "tool-call") {
539
+ block.input += event.delta.partial_json;
540
+ controller.enqueue({
541
+ type: "tool-input-delta",
542
+ id: block.toolCallId,
543
+ delta: event.delta.partial_json
544
+ });
545
+ }
546
+ break;
547
+ }
548
+ case "content_block_stop": {
549
+ const block = contentBlocks[event.index];
550
+ if (block?.type === "text") {
551
+ controller.enqueue({
552
+ type: "text-end",
553
+ id: block.id
554
+ });
555
+ } else if (block?.type === "tool-call") {
556
+ controller.enqueue({
557
+ type: "tool-input-end",
558
+ id: block.toolCallId
559
+ });
560
+ controller.enqueue({
561
+ type: "tool-call",
562
+ toolCallId: block.toolCallId,
563
+ toolName: block.toolName,
564
+ input: block.input === "" ? "{}" : block.input
565
+ });
566
+ }
567
+ delete contentBlocks[event.index];
568
+ break;
569
+ }
570
+ case "message_delta":
571
+ if (event.usage) {
572
+ usage.outputTokens = event.usage.output_tokens;
573
+ usage.totalTokens = (usage.inputTokens || 0) + event.usage.output_tokens;
574
+ }
575
+ if (event.delta.stop_reason) {
576
+ finishReason = self.convertFinishReason(event.delta.stop_reason);
577
+ }
578
+ break;
579
+ case "message_stop": {
560
580
  controller.enqueue({
561
- type: "tool-call",
562
- toolCallId: block.id,
563
- toolName: block.name,
564
- input: JSON.stringify(block.input)
581
+ type: "finish",
582
+ finishReason,
583
+ usage
565
584
  });
585
+ break;
566
586
  }
567
587
  }
568
- controller.enqueue({
569
- type: "finish",
570
- finishReason,
571
- usage
572
- });
573
- break;
588
+ } catch {
574
589
  }
590
+ });
591
+ anthropicStream.on("end", () => {
592
+ resolve2();
593
+ });
594
+ anthropicStream.on("error", (error) => {
595
+ reject(error);
596
+ });
597
+ });
598
+ for (const [, block] of Object.entries(contentBlocks)) {
599
+ if (block.type === "tool-call") {
600
+ controller.enqueue({
601
+ type: "tool-input-end",
602
+ id: block.toolCallId
603
+ });
604
+ controller.enqueue({
605
+ type: "tool-call",
606
+ toolCallId: block.toolCallId,
607
+ toolName: block.toolName,
608
+ input: block.input === "" ? "{}" : block.input
609
+ });
575
610
  }
576
611
  }
577
612
  controller.close();
578
613
  } catch (error) {
614
+ for (const [, block] of Object.entries(contentBlocks)) {
615
+ if (block.type === "tool-call") {
616
+ controller.enqueue({
617
+ type: "tool-input-end",
618
+ id: block.toolCallId
619
+ });
620
+ controller.enqueue({
621
+ type: "tool-call",
622
+ toolCallId: block.toolCallId,
623
+ toolName: block.toolName,
624
+ input: block.input === "" ? "{}" : block.input
625
+ });
626
+ }
627
+ }
579
628
  if (!isRetry && self.isTokenError(error)) {
580
629
  self.directAccessClient.invalidateToken();
581
630
  controller.enqueue({
@@ -613,6 +662,740 @@ var GitLabAgenticLanguageModel = class {
613
662
  }
614
663
  };
615
664
 
665
+ // src/gitlab-openai-language-model.ts
666
+ var import_openai = __toESM(require("openai"));
667
+
668
+ // src/model-mappings.ts
669
+ var MODEL_MAPPINGS = {
670
+ // Anthropic models
671
+ "duo-chat-opus-4-5": { provider: "anthropic", model: "claude-opus-4-5-20251101" },
672
+ "duo-chat-sonnet-4-5": { provider: "anthropic", model: "claude-sonnet-4-5-20250929" },
673
+ "duo-chat-haiku-4-5": { provider: "anthropic", model: "claude-haiku-4-5-20251001" },
674
+ // OpenAI models - Chat Completions API
675
+ "duo-chat-gpt-5-1": { provider: "openai", model: "gpt-5.1-2025-11-13", openaiApiType: "chat" },
676
+ "duo-chat-gpt-5-mini": {
677
+ provider: "openai",
678
+ model: "gpt-5-mini-2025-08-07",
679
+ openaiApiType: "chat"
680
+ },
681
+ // OpenAI models - Responses API (Codex models)
682
+ "duo-chat-gpt-5-codex": { provider: "openai", model: "gpt-5-codex", openaiApiType: "responses" },
683
+ "duo-chat-gpt-5-2-codex": {
684
+ provider: "openai",
685
+ model: "gpt-5.2-codex",
686
+ openaiApiType: "responses"
687
+ }
688
+ };
689
+ function getModelMapping(modelId) {
690
+ return MODEL_MAPPINGS[modelId];
691
+ }
692
+ function getProviderForModelId(modelId) {
693
+ return MODEL_MAPPINGS[modelId]?.provider;
694
+ }
695
+ function getValidModelsForProvider(provider) {
696
+ return Object.values(MODEL_MAPPINGS).filter((m) => m.provider === provider).map((m) => m.model);
697
+ }
698
+ function getAnthropicModelForModelId(modelId) {
699
+ const mapping = MODEL_MAPPINGS[modelId];
700
+ return mapping?.provider === "anthropic" ? mapping.model : void 0;
701
+ }
702
+ function getOpenAIModelForModelId(modelId) {
703
+ const mapping = MODEL_MAPPINGS[modelId];
704
+ return mapping?.provider === "openai" ? mapping.model : void 0;
705
+ }
706
+ function getOpenAIApiType(modelId) {
707
+ const mapping = MODEL_MAPPINGS[modelId];
708
+ return mapping?.openaiApiType ?? "chat";
709
+ }
710
+ function isResponsesApiModel(modelId) {
711
+ return getOpenAIApiType(modelId) === "responses";
712
+ }
713
+ var MODEL_ID_TO_ANTHROPIC_MODEL = Object.fromEntries(
714
+ Object.entries(MODEL_MAPPINGS).filter(([, v]) => v.provider === "anthropic").map(([k, v]) => [k, v.model])
715
+ );
716
+
717
+ // src/gitlab-openai-language-model.ts
718
+ var GitLabOpenAILanguageModel = class {
719
+ specificationVersion = "v2";
720
+ modelId;
721
+ supportedUrls = {};
722
+ config;
723
+ directAccessClient;
724
+ useResponsesApi;
725
+ openaiClient = null;
726
+ constructor(modelId, config) {
727
+ this.modelId = modelId;
728
+ this.config = config;
729
+ this.useResponsesApi = config.useResponsesApi ?? isResponsesApiModel(modelId);
730
+ this.directAccessClient = new GitLabDirectAccessClient({
731
+ instanceUrl: config.instanceUrl,
732
+ getHeaders: config.getHeaders,
733
+ refreshApiKey: config.refreshApiKey,
734
+ fetch: config.fetch,
735
+ featureFlags: config.featureFlags,
736
+ aiGatewayUrl: config.aiGatewayUrl
737
+ });
738
+ }
739
+ get provider() {
740
+ return this.config.provider;
741
+ }
742
+ async getOpenAIClient(forceRefresh = false) {
743
+ const tokenData = await this.directAccessClient.getDirectAccessToken(forceRefresh);
744
+ const { "x-api-key": _removed, ...filteredHeaders } = tokenData.headers;
745
+ this.openaiClient = new import_openai.default({
746
+ apiKey: tokenData.token,
747
+ baseURL: this.directAccessClient.getOpenAIProxyUrl(),
748
+ defaultHeaders: filteredHeaders
749
+ });
750
+ return this.openaiClient;
751
+ }
752
+ isTokenError(error) {
753
+ if (error instanceof import_openai.default.APIError) {
754
+ if (error.status === 401) {
755
+ return true;
756
+ }
757
+ const message = error.message?.toLowerCase() || "";
758
+ if (message.includes("token") && (message.includes("expired") || message.includes("revoked") || message.includes("invalid"))) {
759
+ return true;
760
+ }
761
+ }
762
+ return false;
763
+ }
764
+ convertTools(tools) {
765
+ if (!tools || tools.length === 0) {
766
+ return void 0;
767
+ }
768
+ return tools.filter((tool) => tool.type === "function").map((tool) => {
769
+ const schema = tool.inputSchema;
770
+ return {
771
+ type: "function",
772
+ function: {
773
+ name: tool.name,
774
+ description: tool.description || "",
775
+ // Ensure the schema has type: 'object' as OpenAI requires it
776
+ parameters: {
777
+ type: "object",
778
+ ...schema
779
+ }
780
+ }
781
+ };
782
+ });
783
+ }
784
+ convertToolChoice(toolChoice) {
785
+ if (!toolChoice) {
786
+ return void 0;
787
+ }
788
+ switch (toolChoice.type) {
789
+ case "auto":
790
+ return "auto";
791
+ case "none":
792
+ return "none";
793
+ case "required":
794
+ return "required";
795
+ case "tool":
796
+ return { type: "function", function: { name: toolChoice.toolName } };
797
+ default:
798
+ return void 0;
799
+ }
800
+ }
801
+ convertPrompt(prompt) {
802
+ const messages = [];
803
+ for (const message of prompt) {
804
+ if (message.role === "system") {
805
+ messages.push({ role: "system", content: message.content });
806
+ continue;
807
+ }
808
+ if (message.role === "user") {
809
+ const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text);
810
+ if (textParts.length > 0) {
811
+ messages.push({ role: "user", content: textParts.join("\n") });
812
+ }
813
+ } else if (message.role === "assistant") {
814
+ const textParts = [];
815
+ const toolCalls = [];
816
+ for (const part of message.content) {
817
+ if (part.type === "text") {
818
+ textParts.push(part.text);
819
+ } else if (part.type === "tool-call") {
820
+ toolCalls.push({
821
+ id: part.toolCallId,
822
+ type: "function",
823
+ function: {
824
+ name: part.toolName,
825
+ arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input)
826
+ }
827
+ });
828
+ }
829
+ }
830
+ const assistantMessage = {
831
+ role: "assistant",
832
+ content: textParts.length > 0 ? textParts.join("\n") : null
833
+ };
834
+ if (toolCalls.length > 0) {
835
+ assistantMessage.tool_calls = toolCalls;
836
+ }
837
+ messages.push(assistantMessage);
838
+ } else if (message.role === "tool") {
839
+ for (const part of message.content) {
840
+ if (part.type === "tool-result") {
841
+ let resultContent;
842
+ if (part.output.type === "text") {
843
+ resultContent = part.output.value;
844
+ } else if (part.output.type === "json") {
845
+ resultContent = JSON.stringify(part.output.value);
846
+ } else if (part.output.type === "error-text") {
847
+ resultContent = part.output.value;
848
+ } else if (part.output.type === "error-json") {
849
+ resultContent = JSON.stringify(part.output.value);
850
+ } else {
851
+ resultContent = JSON.stringify(part.output);
852
+ }
853
+ messages.push({
854
+ role: "tool",
855
+ tool_call_id: part.toolCallId,
856
+ content: resultContent
857
+ });
858
+ }
859
+ }
860
+ }
861
+ }
862
+ return messages;
863
+ }
864
+ convertFinishReason(finishReason) {
865
+ switch (finishReason) {
866
+ case "stop":
867
+ return "stop";
868
+ case "length":
869
+ return "length";
870
+ case "tool_calls":
871
+ return "tool-calls";
872
+ case "content_filter":
873
+ return "content-filter";
874
+ default:
875
+ return "unknown";
876
+ }
877
+ }
878
+ /**
879
+ * Convert tools to Responses API format
880
+ */
881
+ convertToolsForResponses(tools) {
882
+ if (!tools || tools.length === 0) {
883
+ return void 0;
884
+ }
885
+ return tools.filter((tool) => tool.type === "function").map((tool) => {
886
+ const schema = { ...tool.inputSchema };
887
+ delete schema["$schema"];
888
+ return {
889
+ type: "function",
890
+ name: tool.name,
891
+ description: tool.description || "",
892
+ parameters: schema,
893
+ strict: false
894
+ };
895
+ });
896
+ }
897
+ /**
898
+ * Convert prompt to Responses API input format
899
+ */
900
+ convertPromptForResponses(prompt) {
901
+ const items = [];
902
+ for (const message of prompt) {
903
+ if (message.role === "system") {
904
+ continue;
905
+ }
906
+ if (message.role === "user") {
907
+ const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text);
908
+ if (textParts.length > 0) {
909
+ items.push({
910
+ type: "message",
911
+ role: "user",
912
+ content: textParts.map((text) => ({ type: "input_text", text }))
913
+ });
914
+ }
915
+ } else if (message.role === "assistant") {
916
+ const textParts = [];
917
+ for (const part of message.content) {
918
+ if (part.type === "text") {
919
+ textParts.push(part.text);
920
+ } else if (part.type === "tool-call") {
921
+ items.push({
922
+ type: "function_call",
923
+ call_id: part.toolCallId,
924
+ name: part.toolName,
925
+ arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input)
926
+ });
927
+ }
928
+ }
929
+ if (textParts.length > 0) {
930
+ items.push({
931
+ type: "message",
932
+ role: "assistant",
933
+ content: [{ type: "output_text", text: textParts.join("\n"), annotations: [] }]
934
+ });
935
+ }
936
+ } else if (message.role === "tool") {
937
+ for (const part of message.content) {
938
+ if (part.type === "tool-result") {
939
+ let resultContent;
940
+ if (part.output.type === "text") {
941
+ resultContent = part.output.value;
942
+ } else if (part.output.type === "json") {
943
+ resultContent = JSON.stringify(part.output.value);
944
+ } else if (part.output.type === "error-text") {
945
+ resultContent = part.output.value;
946
+ } else if (part.output.type === "error-json") {
947
+ resultContent = JSON.stringify(part.output.value);
948
+ } else {
949
+ resultContent = JSON.stringify(part.output);
950
+ }
951
+ items.push({
952
+ type: "function_call_output",
953
+ call_id: part.toolCallId,
954
+ output: resultContent
955
+ });
956
+ }
957
+ }
958
+ }
959
+ }
960
+ return items;
961
+ }
962
+ /**
963
+ * Extract system instructions from prompt
964
+ */
965
+ extractSystemInstructions(prompt) {
966
+ const systemMessages = prompt.filter((m) => m.role === "system").map((m) => m.content).join("\n");
967
+ return systemMessages || void 0;
968
+ }
969
+ /**
970
+ * Convert Responses API status to finish reason
971
+ * Note: Responses API returns 'completed' even when making tool calls,
972
+ * so we need to check the content for tool calls separately.
973
+ */
974
+ convertResponsesStatus(status, hasToolCalls = false) {
975
+ if (hasToolCalls) {
976
+ return "tool-calls";
977
+ }
978
+ switch (status) {
979
+ case "completed":
980
+ return "stop";
981
+ case "incomplete":
982
+ return "length";
983
+ case "cancelled":
984
+ return "stop";
985
+ case "failed":
986
+ return "error";
987
+ default:
988
+ return "unknown";
989
+ }
990
+ }
991
+ async doGenerate(options) {
992
+ if (this.useResponsesApi) {
993
+ return this.doGenerateWithResponsesApi(options, false);
994
+ }
995
+ return this.doGenerateWithChatApi(options, false);
996
+ }
997
+ async doGenerateWithChatApi(options, isRetry) {
998
+ const client = await this.getOpenAIClient(isRetry);
999
+ const messages = this.convertPrompt(options.prompt);
1000
+ const tools = this.convertTools(options.tools);
1001
+ const toolChoice = options.toolChoice?.type !== "none" ? this.convertToolChoice(options.toolChoice) : void 0;
1002
+ const openaiModel = this.config.openaiModel || "gpt-4o";
1003
+ const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
1004
+ try {
1005
+ const response = await client.chat.completions.create({
1006
+ model: openaiModel,
1007
+ max_completion_tokens: maxTokens,
1008
+ messages,
1009
+ tools,
1010
+ tool_choice: tools ? toolChoice : void 0,
1011
+ temperature: options.temperature,
1012
+ top_p: options.topP,
1013
+ stop: options.stopSequences
1014
+ });
1015
+ const choice = response.choices[0];
1016
+ const content = [];
1017
+ if (choice?.message.content) {
1018
+ content.push({ type: "text", text: choice.message.content });
1019
+ }
1020
+ if (choice?.message.tool_calls) {
1021
+ for (const toolCall of choice.message.tool_calls) {
1022
+ if (toolCall.type === "function") {
1023
+ content.push({
1024
+ type: "tool-call",
1025
+ toolCallId: toolCall.id,
1026
+ toolName: toolCall.function.name,
1027
+ input: toolCall.function.arguments
1028
+ });
1029
+ }
1030
+ }
1031
+ }
1032
+ const usage = {
1033
+ inputTokens: response.usage?.prompt_tokens || 0,
1034
+ outputTokens: response.usage?.completion_tokens || 0,
1035
+ totalTokens: response.usage?.total_tokens || 0
1036
+ };
1037
+ return {
1038
+ content,
1039
+ finishReason: this.convertFinishReason(choice?.finish_reason),
1040
+ usage,
1041
+ warnings: []
1042
+ };
1043
+ } catch (error) {
1044
+ if (!isRetry && this.isTokenError(error)) {
1045
+ this.directAccessClient.invalidateToken();
1046
+ return this.doGenerateWithChatApi(options, true);
1047
+ }
1048
+ if (error instanceof import_openai.default.APIError) {
1049
+ throw new GitLabError({
1050
+ message: `OpenAI API error: ${error.message}`,
1051
+ cause: error
1052
+ });
1053
+ }
1054
+ throw error;
1055
+ }
1056
+ }
1057
+ async doGenerateWithResponsesApi(options, isRetry) {
1058
+ const client = await this.getOpenAIClient(isRetry);
1059
+ const input = this.convertPromptForResponses(options.prompt);
1060
+ const tools = this.convertToolsForResponses(options.tools);
1061
+ const instructions = this.extractSystemInstructions(options.prompt);
1062
+ const openaiModel = this.config.openaiModel || "gpt-5-codex";
1063
+ const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
1064
+ try {
1065
+ const response = await client.responses.create({
1066
+ model: openaiModel,
1067
+ input,
1068
+ instructions,
1069
+ tools,
1070
+ max_output_tokens: maxTokens,
1071
+ temperature: options.temperature,
1072
+ top_p: options.topP,
1073
+ store: false
1074
+ });
1075
+ const content = [];
1076
+ let hasToolCalls = false;
1077
+ for (const item of response.output || []) {
1078
+ if (item.type === "message" && item.role === "assistant") {
1079
+ for (const contentItem of item.content || []) {
1080
+ if (contentItem.type === "output_text") {
1081
+ content.push({ type: "text", text: contentItem.text });
1082
+ }
1083
+ }
1084
+ } else if (item.type === "function_call") {
1085
+ hasToolCalls = true;
1086
+ content.push({
1087
+ type: "tool-call",
1088
+ toolCallId: item.call_id,
1089
+ toolName: item.name,
1090
+ input: item.arguments
1091
+ });
1092
+ }
1093
+ }
1094
+ const usage = {
1095
+ inputTokens: response.usage?.input_tokens || 0,
1096
+ outputTokens: response.usage?.output_tokens || 0,
1097
+ totalTokens: response.usage?.total_tokens || 0
1098
+ };
1099
+ return {
1100
+ content,
1101
+ finishReason: this.convertResponsesStatus(response.status, hasToolCalls),
1102
+ usage,
1103
+ warnings: []
1104
+ };
1105
+ } catch (error) {
1106
+ if (!isRetry && this.isTokenError(error)) {
1107
+ this.directAccessClient.invalidateToken();
1108
+ return this.doGenerateWithResponsesApi(options, true);
1109
+ }
1110
+ if (error instanceof import_openai.default.APIError) {
1111
+ throw new GitLabError({
1112
+ message: `OpenAI API error: ${error.message}`,
1113
+ cause: error
1114
+ });
1115
+ }
1116
+ throw error;
1117
+ }
1118
+ }
1119
+ async doStream(options) {
1120
+ if (this.useResponsesApi) {
1121
+ return this.doStreamWithResponsesApi(options, false);
1122
+ }
1123
+ return this.doStreamWithChatApi(options, false);
1124
+ }
1125
+ async doStreamWithChatApi(options, isRetry) {
1126
+ const client = await this.getOpenAIClient(isRetry);
1127
+ const messages = this.convertPrompt(options.prompt);
1128
+ const tools = this.convertTools(options.tools);
1129
+ const toolChoice = options.toolChoice?.type !== "none" ? this.convertToolChoice(options.toolChoice) : void 0;
1130
+ const openaiModel = this.config.openaiModel || "gpt-4o";
1131
+ const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
1132
+ const requestBody = {
1133
+ model: openaiModel,
1134
+ max_completion_tokens: maxTokens,
1135
+ messages,
1136
+ tools,
1137
+ tool_choice: tools ? toolChoice : void 0,
1138
+ temperature: options.temperature,
1139
+ top_p: options.topP,
1140
+ stop: options.stopSequences,
1141
+ stream: true,
1142
+ stream_options: { include_usage: true }
1143
+ };
1144
+ const self = this;
1145
+ const stream = new ReadableStream({
1146
+ start: async (controller) => {
1147
+ const toolCalls = {};
1148
+ const usage = {
1149
+ inputTokens: 0,
1150
+ outputTokens: 0,
1151
+ totalTokens: 0
1152
+ };
1153
+ let finishReason = "unknown";
1154
+ let textStarted = false;
1155
+ const textId = "text-0";
1156
+ try {
1157
+ const openaiStream = await client.chat.completions.create({
1158
+ ...requestBody,
1159
+ stream: true
1160
+ });
1161
+ controller.enqueue({ type: "stream-start", warnings: [] });
1162
+ for await (const chunk of openaiStream) {
1163
+ const choice = chunk.choices?.[0];
1164
+ if (chunk.id && !textStarted) {
1165
+ controller.enqueue({
1166
+ type: "response-metadata",
1167
+ id: chunk.id,
1168
+ modelId: chunk.model
1169
+ });
1170
+ }
1171
+ if (choice?.delta?.content) {
1172
+ if (!textStarted) {
1173
+ controller.enqueue({ type: "text-start", id: textId });
1174
+ textStarted = true;
1175
+ }
1176
+ controller.enqueue({
1177
+ type: "text-delta",
1178
+ id: textId,
1179
+ delta: choice.delta.content
1180
+ });
1181
+ }
1182
+ if (choice?.delta?.tool_calls) {
1183
+ for (const tc of choice.delta.tool_calls) {
1184
+ const idx = tc.index;
1185
+ if (!toolCalls[idx]) {
1186
+ toolCalls[idx] = {
1187
+ id: tc.id || "",
1188
+ name: tc.function?.name || "",
1189
+ arguments: ""
1190
+ };
1191
+ controller.enqueue({
1192
+ type: "tool-input-start",
1193
+ id: toolCalls[idx].id,
1194
+ toolName: toolCalls[idx].name
1195
+ });
1196
+ }
1197
+ if (tc.function?.arguments) {
1198
+ toolCalls[idx].arguments += tc.function.arguments;
1199
+ controller.enqueue({
1200
+ type: "tool-input-delta",
1201
+ id: toolCalls[idx].id,
1202
+ delta: tc.function.arguments
1203
+ });
1204
+ }
1205
+ }
1206
+ }
1207
+ if (choice?.finish_reason) {
1208
+ finishReason = self.convertFinishReason(choice.finish_reason);
1209
+ }
1210
+ if (chunk.usage) {
1211
+ usage.inputTokens = chunk.usage.prompt_tokens || 0;
1212
+ usage.outputTokens = chunk.usage.completion_tokens || 0;
1213
+ usage.totalTokens = chunk.usage.total_tokens || 0;
1214
+ }
1215
+ }
1216
+ if (textStarted) {
1217
+ controller.enqueue({ type: "text-end", id: textId });
1218
+ }
1219
+ for (const [, tc] of Object.entries(toolCalls)) {
1220
+ controller.enqueue({ type: "tool-input-end", id: tc.id });
1221
+ controller.enqueue({
1222
+ type: "tool-call",
1223
+ toolCallId: tc.id,
1224
+ toolName: tc.name,
1225
+ input: tc.arguments || "{}"
1226
+ });
1227
+ }
1228
+ controller.enqueue({ type: "finish", finishReason, usage });
1229
+ controller.close();
1230
+ } catch (error) {
1231
+ if (!isRetry && self.isTokenError(error)) {
1232
+ self.directAccessClient.invalidateToken();
1233
+ controller.enqueue({
1234
+ type: "error",
1235
+ error: new GitLabError({ message: "TOKEN_REFRESH_NEEDED", cause: error })
1236
+ });
1237
+ controller.close();
1238
+ return;
1239
+ }
1240
+ if (error instanceof import_openai.default.APIError) {
1241
+ controller.enqueue({
1242
+ type: "error",
1243
+ error: new GitLabError({
1244
+ message: `OpenAI API error: ${error.message}`,
1245
+ cause: error
1246
+ })
1247
+ });
1248
+ } else {
1249
+ controller.enqueue({ type: "error", error });
1250
+ }
1251
+ controller.close();
1252
+ }
1253
+ }
1254
+ });
1255
+ return { stream, request: { body: requestBody } };
1256
+ }
1257
+ async doStreamWithResponsesApi(options, isRetry) {
1258
+ const client = await this.getOpenAIClient(isRetry);
1259
+ const input = this.convertPromptForResponses(options.prompt);
1260
+ const tools = this.convertToolsForResponses(options.tools);
1261
+ const instructions = this.extractSystemInstructions(options.prompt);
1262
+ const openaiModel = this.config.openaiModel || "gpt-5-codex";
1263
+ const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
1264
+ const requestBody = {
1265
+ model: openaiModel,
1266
+ input,
1267
+ instructions,
1268
+ tools,
1269
+ max_output_tokens: maxTokens,
1270
+ temperature: options.temperature,
1271
+ top_p: options.topP,
1272
+ store: false,
1273
+ stream: true
1274
+ };
1275
+ const self = this;
1276
+ const stream = new ReadableStream({
1277
+ start: async (controller) => {
1278
+ const toolCalls = {};
1279
+ const usage = {
1280
+ inputTokens: 0,
1281
+ outputTokens: 0,
1282
+ totalTokens: 0
1283
+ };
1284
+ let finishReason = "unknown";
1285
+ let textStarted = false;
1286
+ const textId = "text-0";
1287
+ try {
1288
+ const openaiStream = await client.responses.create({
1289
+ ...requestBody,
1290
+ stream: true
1291
+ });
1292
+ controller.enqueue({ type: "stream-start", warnings: [] });
1293
+ for await (const event of openaiStream) {
1294
+ if (event.type === "response.created") {
1295
+ controller.enqueue({
1296
+ type: "response-metadata",
1297
+ id: event.response.id,
1298
+ modelId: event.response.model
1299
+ });
1300
+ } else if (event.type === "response.output_item.added") {
1301
+ if (event.item.type === "function_call") {
1302
+ const outputIndex = event.output_index;
1303
+ const callId = event.item.call_id;
1304
+ toolCalls[outputIndex] = {
1305
+ callId,
1306
+ name: event.item.name,
1307
+ arguments: ""
1308
+ };
1309
+ controller.enqueue({
1310
+ type: "tool-input-start",
1311
+ id: callId,
1312
+ toolName: event.item.name
1313
+ });
1314
+ }
1315
+ } else if (event.type === "response.output_text.delta") {
1316
+ if (!textStarted) {
1317
+ controller.enqueue({ type: "text-start", id: textId });
1318
+ textStarted = true;
1319
+ }
1320
+ controller.enqueue({
1321
+ type: "text-delta",
1322
+ id: textId,
1323
+ delta: event.delta
1324
+ });
1325
+ } else if (event.type === "response.function_call_arguments.delta") {
1326
+ const outputIndex = event.output_index;
1327
+ const tc = toolCalls[outputIndex];
1328
+ if (tc) {
1329
+ tc.arguments += event.delta;
1330
+ controller.enqueue({
1331
+ type: "tool-input-delta",
1332
+ id: tc.callId,
1333
+ delta: event.delta
1334
+ });
1335
+ }
1336
+ } else if (event.type === "response.function_call_arguments.done") {
1337
+ const outputIndex = event.output_index;
1338
+ const tc = toolCalls[outputIndex];
1339
+ if (tc) {
1340
+ tc.arguments = event.arguments;
1341
+ }
1342
+ } else if (event.type === "response.completed") {
1343
+ const hasToolCalls2 = Object.keys(toolCalls).length > 0;
1344
+ finishReason = self.convertResponsesStatus(event.response.status, hasToolCalls2);
1345
+ if (event.response.usage) {
1346
+ usage.inputTokens = event.response.usage.input_tokens || 0;
1347
+ usage.outputTokens = event.response.usage.output_tokens || 0;
1348
+ usage.totalTokens = event.response.usage.total_tokens || 0;
1349
+ }
1350
+ }
1351
+ }
1352
+ if (textStarted) {
1353
+ controller.enqueue({ type: "text-end", id: textId });
1354
+ }
1355
+ const hasToolCalls = Object.keys(toolCalls).length > 0;
1356
+ if (hasToolCalls && finishReason === "stop") {
1357
+ finishReason = "tool-calls";
1358
+ }
1359
+ for (const tc of Object.values(toolCalls)) {
1360
+ controller.enqueue({ type: "tool-input-end", id: tc.callId });
1361
+ controller.enqueue({
1362
+ type: "tool-call",
1363
+ toolCallId: tc.callId,
1364
+ toolName: tc.name,
1365
+ input: tc.arguments || "{}"
1366
+ });
1367
+ }
1368
+ controller.enqueue({ type: "finish", finishReason, usage });
1369
+ controller.close();
1370
+ } catch (error) {
1371
+ if (!isRetry && self.isTokenError(error)) {
1372
+ self.directAccessClient.invalidateToken();
1373
+ controller.enqueue({
1374
+ type: "error",
1375
+ error: new GitLabError({ message: "TOKEN_REFRESH_NEEDED", cause: error })
1376
+ });
1377
+ controller.close();
1378
+ return;
1379
+ }
1380
+ if (error instanceof import_openai.default.APIError) {
1381
+ controller.enqueue({
1382
+ type: "error",
1383
+ error: new GitLabError({
1384
+ message: `OpenAI API error: ${error.message}`,
1385
+ cause: error
1386
+ })
1387
+ });
1388
+ } else {
1389
+ controller.enqueue({ type: "error", error });
1390
+ }
1391
+ controller.close();
1392
+ }
1393
+ }
1394
+ });
1395
+ return { stream, request: { body: requestBody } };
1396
+ }
1397
+ };
1398
+
616
1399
  // src/gitlab-oauth-types.ts
617
1400
  var BUNDLED_CLIENT_ID = "36f2a70cddeb5a0889d4fd8295c241b7e9848e89cf9e599d0eed2d8e5350fbf5";
618
1401
  var GITLAB_COM_URL = "https://gitlab.com";
@@ -775,16 +1558,6 @@ var GitLabOAuthManager = class {
775
1558
  }
776
1559
  };
777
1560
 
778
- // src/model-mappings.ts
779
- var MODEL_ID_TO_ANTHROPIC_MODEL = {
780
- "duo-chat-opus-4-5": "claude-opus-4-5-20251101",
781
- "duo-chat-sonnet-4-5": "claude-sonnet-4-5-20250929",
782
- "duo-chat-haiku-4-5": "claude-haiku-4-5-20251001"
783
- };
784
- function getAnthropicModelForModelId(modelId) {
785
- return MODEL_ID_TO_ANTHROPIC_MODEL[modelId];
786
- }
787
-
788
1561
  // src/gitlab-provider.ts
789
1562
  var fs = __toESM(require("fs"));
790
1563
  var path = __toESM(require("path"));
@@ -920,21 +1693,44 @@ function createGitLab(options = {}) {
920
1693
  getApiKey().catch(() => {
921
1694
  });
922
1695
  const createAgenticChatModel = (modelId, agenticOptions) => {
1696
+ const mapping = getModelMapping(modelId);
1697
+ if (!mapping) {
1698
+ throw new GitLabError({
1699
+ message: `Unknown model ID: ${modelId}. Model must be registered in MODEL_MAPPINGS.`
1700
+ });
1701
+ }
1702
+ if (agenticOptions?.providerModel) {
1703
+ const validModels = getValidModelsForProvider(mapping.provider);
1704
+ if (!validModels.includes(agenticOptions.providerModel)) {
1705
+ throw new GitLabError({
1706
+ message: `Invalid providerModel '${agenticOptions.providerModel}' for provider '${mapping.provider}'. Valid models: ${validModels.join(", ")}`
1707
+ });
1708
+ }
1709
+ }
923
1710
  const featureFlags = {
924
1711
  DuoAgentPlatformNext: true,
925
1712
  ...options.featureFlags,
926
1713
  ...agenticOptions?.featureFlags
927
1714
  };
928
- return new GitLabAgenticLanguageModel(modelId, {
1715
+ const baseConfig = {
929
1716
  provider: `${providerName}.agentic`,
930
1717
  instanceUrl,
931
1718
  getHeaders,
932
1719
  refreshApiKey,
933
1720
  fetch: options.fetch,
934
- anthropicModel: agenticOptions?.anthropicModel ?? getAnthropicModelForModelId(modelId),
935
1721
  maxTokens: agenticOptions?.maxTokens,
936
1722
  featureFlags,
937
1723
  aiGatewayUrl: options.aiGatewayUrl
1724
+ };
1725
+ if (mapping.provider === "openai") {
1726
+ return new GitLabOpenAILanguageModel(modelId, {
1727
+ ...baseConfig,
1728
+ openaiModel: agenticOptions?.providerModel ?? mapping.model
1729
+ });
1730
+ }
1731
+ return new GitLabAnthropicLanguageModel(modelId, {
1732
+ ...baseConfig,
1733
+ anthropicModel: agenticOptions?.providerModel ?? mapping.model
938
1734
  });
939
1735
  };
940
1736
  const createDefaultModel = (modelId) => {
@@ -1045,7 +1841,7 @@ var GitLabProjectCache = class {
1045
1841
  };
1046
1842
 
1047
1843
  // src/gitlab-project-detector.ts
1048
- var debugLog2 = (..._args) => {
1844
+ var debugLog = (..._args) => {
1049
1845
  };
1050
1846
  var GitLabProjectDetector = class {
1051
1847
  config;
@@ -1074,35 +1870,35 @@ var GitLabProjectDetector = class {
1074
1870
  return cached;
1075
1871
  }
1076
1872
  try {
1077
- debugLog2(`[GitLabProjectDetector] Getting git remote URL from: ${workingDirectory}`);
1873
+ debugLog(`[GitLabProjectDetector] Getting git remote URL from: ${workingDirectory}`);
1078
1874
  const remoteUrl = await this.getGitRemoteUrl(workingDirectory, remoteName);
1079
1875
  if (!remoteUrl) {
1080
- debugLog2(`[GitLabProjectDetector] No git remote URL found`);
1876
+ debugLog(`[GitLabProjectDetector] No git remote URL found`);
1081
1877
  return null;
1082
1878
  }
1083
- debugLog2(`[GitLabProjectDetector] Git remote URL: ${remoteUrl}`);
1084
- debugLog2(
1879
+ debugLog(`[GitLabProjectDetector] Git remote URL: ${remoteUrl}`);
1880
+ debugLog(
1085
1881
  `[GitLabProjectDetector] Parsing project path from URL (instance: ${this.config.instanceUrl})`
1086
1882
  );
1087
1883
  const projectPath = this.parseGitRemoteUrl(remoteUrl, this.config.instanceUrl);
1088
1884
  if (!projectPath) {
1089
- debugLog2(
1885
+ debugLog(
1090
1886
  `[GitLabProjectDetector] Could not parse project path from URL (remote doesn't match instance)`
1091
1887
  );
1092
1888
  return null;
1093
1889
  }
1094
- debugLog2(`[GitLabProjectDetector] Parsed project path: ${projectPath}`);
1095
- debugLog2(`[GitLabProjectDetector] Fetching project from GitLab API: ${projectPath}`);
1890
+ debugLog(`[GitLabProjectDetector] Parsed project path: ${projectPath}`);
1891
+ debugLog(`[GitLabProjectDetector] Fetching project from GitLab API: ${projectPath}`);
1096
1892
  const project = await this.getProjectByPath(projectPath);
1097
- debugLog2(`[GitLabProjectDetector] \u2713 Project fetched successfully:`, project);
1893
+ debugLog(`[GitLabProjectDetector] \u2713 Project fetched successfully:`, project);
1098
1894
  this.cache.set(cacheKey, project);
1099
1895
  return project;
1100
1896
  } catch (error) {
1101
1897
  if (error instanceof GitLabError) {
1102
- debugLog2(`[GitLabProjectDetector] GitLab API error:`, error.message || error);
1898
+ debugLog(`[GitLabProjectDetector] GitLab API error:`, error.message || error);
1103
1899
  return null;
1104
1900
  }
1105
- debugLog2(`[GitLabProjectDetector] Unexpected error:`, error);
1901
+ debugLog(`[GitLabProjectDetector] Unexpected error:`, error);
1106
1902
  console.warn(`Failed to auto-detect GitLab project: ${error}`);
1107
1903
  return null;
1108
1904
  }
@@ -1234,17 +2030,25 @@ var GitLabProjectDetector = class {
1234
2030
  BUNDLED_CLIENT_ID,
1235
2031
  DEFAULT_AI_GATEWAY_URL,
1236
2032
  GITLAB_COM_URL,
1237
- GitLabAgenticLanguageModel,
2033
+ GitLabAnthropicLanguageModel,
1238
2034
  GitLabDirectAccessClient,
1239
2035
  GitLabError,
1240
2036
  GitLabOAuthManager,
2037
+ GitLabOpenAILanguageModel,
1241
2038
  GitLabProjectCache,
1242
2039
  GitLabProjectDetector,
1243
2040
  MODEL_ID_TO_ANTHROPIC_MODEL,
2041
+ MODEL_MAPPINGS,
1244
2042
  OAUTH_SCOPES,
1245
2043
  TOKEN_EXPIRY_SKEW_MS,
1246
2044
  createGitLab,
1247
2045
  getAnthropicModelForModelId,
1248
- gitlab
2046
+ getModelMapping,
2047
+ getOpenAIApiType,
2048
+ getOpenAIModelForModelId,
2049
+ getProviderForModelId,
2050
+ getValidModelsForProvider,
2051
+ gitlab,
2052
+ isResponsesApiModel
1249
2053
  });
1250
2054
  //# sourceMappingURL=index.js.map