@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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- // src/gitlab-agentic-language-model.ts
1
+ // src/gitlab-anthropic-language-model.ts
2
2
  import Anthropic from "@anthropic-ai/sdk";
3
3
 
4
4
  // src/gitlab-direct-access.ts
@@ -133,6 +133,15 @@ var GitLabDirectAccessClient = class {
133
133
  const baseUrl = this.aiGatewayUrl.replace(/\/$/, "");
134
134
  return `${baseUrl}/ai/v1/proxy/anthropic/`;
135
135
  }
136
+ /**
137
+ * Get the OpenAI proxy base URL
138
+ * Note: The OpenAI SDK expects a base URL like https://api.openai.com/v1
139
+ * and appends paths like /chat/completions. So we need /v1 at the end.
140
+ */
141
+ getOpenAIProxyUrl() {
142
+ const baseUrl = this.aiGatewayUrl.replace(/\/$/, "");
143
+ return `${baseUrl}/ai/v1/proxy/openai/v1`;
144
+ }
136
145
  /**
137
146
  * Invalidate the cached token
138
147
  */
@@ -142,10 +151,8 @@ var GitLabDirectAccessClient = class {
142
151
  }
143
152
  };
144
153
 
145
- // src/gitlab-agentic-language-model.ts
146
- var debugLog = (..._args) => {
147
- };
148
- var GitLabAgenticLanguageModel = class {
154
+ // src/gitlab-anthropic-language-model.ts
155
+ var GitLabAnthropicLanguageModel = class {
149
156
  specificationVersion = "v2";
150
157
  modelId;
151
158
  supportedUrls = {};
@@ -173,14 +180,7 @@ var GitLabAgenticLanguageModel = class {
173
180
  */
174
181
  async getAnthropicClient(forceRefresh = false) {
175
182
  const tokenData = await this.directAccessClient.getDirectAccessToken(forceRefresh);
176
- debugLog("[gitlab-ai-provider] Token headers from GitLab:", tokenData.headers);
177
- debugLog("[gitlab-ai-provider] Proxy URL:", this.directAccessClient.getAnthropicProxyUrl());
178
183
  const { "x-api-key": _removed, ...filteredHeaders } = tokenData.headers;
179
- if (_removed) {
180
- debugLog(
181
- "[gitlab-ai-provider] Filtered out x-api-key from headers (using authToken instead)"
182
- );
183
- }
184
184
  this.anthropicClient = new Anthropic({
185
185
  apiKey: null,
186
186
  authToken: tokenData.token,
@@ -419,114 +419,155 @@ var GitLabAgenticLanguageModel = class {
419
419
  const self = this;
420
420
  const stream = new ReadableStream({
421
421
  start: async (controller) => {
422
+ const contentBlocks = {};
423
+ const usage = {
424
+ inputTokens: 0,
425
+ outputTokens: 0,
426
+ totalTokens: 0
427
+ };
428
+ let finishReason = "unknown";
422
429
  try {
423
- const anthropicStream = client.messages.stream(requestBody);
424
- let currentTextBlockId = null;
425
- let currentToolBlockId = null;
426
- let currentToolName = null;
427
- const usage = {
428
- inputTokens: 0,
429
- outputTokens: 0,
430
- totalTokens: 0
431
- };
432
- let finishReason = "unknown";
430
+ const anthropicStream = client.messages.stream(requestBody, {
431
+ signal: options.abortSignal
432
+ });
433
433
  controller.enqueue({
434
434
  type: "stream-start",
435
435
  warnings: []
436
436
  });
437
- for await (const event of anthropicStream) {
438
- switch (event.type) {
439
- case "message_start":
440
- if (event.message.usage) {
441
- usage.inputTokens = event.message.usage.input_tokens;
442
- }
443
- controller.enqueue({
444
- type: "response-metadata",
445
- id: event.message.id,
446
- modelId: event.message.model
447
- });
448
- break;
449
- case "content_block_start":
450
- if (event.content_block.type === "text") {
451
- currentTextBlockId = `text-${event.index}`;
452
- controller.enqueue({
453
- type: "text-start",
454
- id: currentTextBlockId
455
- });
456
- } else if (event.content_block.type === "tool_use") {
457
- currentToolBlockId = event.content_block.id;
458
- currentToolName = event.content_block.name;
459
- controller.enqueue({
460
- type: "tool-input-start",
461
- id: currentToolBlockId,
462
- toolName: currentToolName
463
- });
464
- }
465
- break;
466
- case "content_block_delta":
467
- if (event.delta.type === "text_delta" && currentTextBlockId) {
468
- controller.enqueue({
469
- type: "text-delta",
470
- id: currentTextBlockId,
471
- delta: event.delta.text
472
- });
473
- } else if (event.delta.type === "input_json_delta" && currentToolBlockId) {
474
- controller.enqueue({
475
- type: "tool-input-delta",
476
- id: currentToolBlockId,
477
- delta: event.delta.partial_json
478
- });
479
- }
480
- break;
481
- case "content_block_stop":
482
- if (currentTextBlockId) {
483
- controller.enqueue({
484
- type: "text-end",
485
- id: currentTextBlockId
486
- });
487
- currentTextBlockId = null;
488
- }
489
- if (currentToolBlockId) {
490
- controller.enqueue({
491
- type: "tool-input-end",
492
- id: currentToolBlockId
493
- });
494
- currentToolBlockId = null;
495
- currentToolName = null;
496
- }
497
- break;
498
- case "message_delta":
499
- if (event.usage) {
500
- usage.outputTokens = event.usage.output_tokens;
501
- usage.totalTokens = (usage.inputTokens || 0) + event.usage.output_tokens;
502
- }
503
- if (event.delta.stop_reason) {
504
- finishReason = self.convertFinishReason(event.delta.stop_reason);
505
- }
506
- break;
507
- case "message_stop": {
508
- const finalMessage = await anthropicStream.finalMessage();
509
- for (const block of finalMessage.content) {
510
- if (block.type === "tool_use") {
437
+ await new Promise((resolve2, reject) => {
438
+ anthropicStream.on("streamEvent", (event) => {
439
+ try {
440
+ switch (event.type) {
441
+ case "message_start":
442
+ if (event.message.usage) {
443
+ usage.inputTokens = event.message.usage.input_tokens;
444
+ }
445
+ controller.enqueue({
446
+ type: "response-metadata",
447
+ id: event.message.id,
448
+ modelId: event.message.model
449
+ });
450
+ break;
451
+ case "content_block_start":
452
+ if (event.content_block.type === "text") {
453
+ const textId = `text-${event.index}`;
454
+ contentBlocks[event.index] = { type: "text", id: textId };
455
+ controller.enqueue({
456
+ type: "text-start",
457
+ id: textId
458
+ });
459
+ } else if (event.content_block.type === "tool_use") {
460
+ contentBlocks[event.index] = {
461
+ type: "tool-call",
462
+ toolCallId: event.content_block.id,
463
+ toolName: event.content_block.name,
464
+ input: ""
465
+ };
466
+ controller.enqueue({
467
+ type: "tool-input-start",
468
+ id: event.content_block.id,
469
+ toolName: event.content_block.name
470
+ });
471
+ }
472
+ break;
473
+ case "content_block_delta": {
474
+ const block = contentBlocks[event.index];
475
+ if (event.delta.type === "text_delta" && block?.type === "text") {
476
+ controller.enqueue({
477
+ type: "text-delta",
478
+ id: block.id,
479
+ delta: event.delta.text
480
+ });
481
+ } else if (event.delta.type === "input_json_delta" && block?.type === "tool-call") {
482
+ block.input += event.delta.partial_json;
483
+ controller.enqueue({
484
+ type: "tool-input-delta",
485
+ id: block.toolCallId,
486
+ delta: event.delta.partial_json
487
+ });
488
+ }
489
+ break;
490
+ }
491
+ case "content_block_stop": {
492
+ const block = contentBlocks[event.index];
493
+ if (block?.type === "text") {
494
+ controller.enqueue({
495
+ type: "text-end",
496
+ id: block.id
497
+ });
498
+ } else if (block?.type === "tool-call") {
499
+ controller.enqueue({
500
+ type: "tool-input-end",
501
+ id: block.toolCallId
502
+ });
503
+ controller.enqueue({
504
+ type: "tool-call",
505
+ toolCallId: block.toolCallId,
506
+ toolName: block.toolName,
507
+ input: block.input === "" ? "{}" : block.input
508
+ });
509
+ }
510
+ delete contentBlocks[event.index];
511
+ break;
512
+ }
513
+ case "message_delta":
514
+ if (event.usage) {
515
+ usage.outputTokens = event.usage.output_tokens;
516
+ usage.totalTokens = (usage.inputTokens || 0) + event.usage.output_tokens;
517
+ }
518
+ if (event.delta.stop_reason) {
519
+ finishReason = self.convertFinishReason(event.delta.stop_reason);
520
+ }
521
+ break;
522
+ case "message_stop": {
511
523
  controller.enqueue({
512
- type: "tool-call",
513
- toolCallId: block.id,
514
- toolName: block.name,
515
- input: JSON.stringify(block.input)
524
+ type: "finish",
525
+ finishReason,
526
+ usage
516
527
  });
528
+ break;
517
529
  }
518
530
  }
519
- controller.enqueue({
520
- type: "finish",
521
- finishReason,
522
- usage
523
- });
524
- break;
531
+ } catch {
525
532
  }
533
+ });
534
+ anthropicStream.on("end", () => {
535
+ resolve2();
536
+ });
537
+ anthropicStream.on("error", (error) => {
538
+ reject(error);
539
+ });
540
+ });
541
+ for (const [, block] of Object.entries(contentBlocks)) {
542
+ if (block.type === "tool-call") {
543
+ controller.enqueue({
544
+ type: "tool-input-end",
545
+ id: block.toolCallId
546
+ });
547
+ controller.enqueue({
548
+ type: "tool-call",
549
+ toolCallId: block.toolCallId,
550
+ toolName: block.toolName,
551
+ input: block.input === "" ? "{}" : block.input
552
+ });
526
553
  }
527
554
  }
528
555
  controller.close();
529
556
  } catch (error) {
557
+ for (const [, block] of Object.entries(contentBlocks)) {
558
+ if (block.type === "tool-call") {
559
+ controller.enqueue({
560
+ type: "tool-input-end",
561
+ id: block.toolCallId
562
+ });
563
+ controller.enqueue({
564
+ type: "tool-call",
565
+ toolCallId: block.toolCallId,
566
+ toolName: block.toolName,
567
+ input: block.input === "" ? "{}" : block.input
568
+ });
569
+ }
570
+ }
530
571
  if (!isRetry && self.isTokenError(error)) {
531
572
  self.directAccessClient.invalidateToken();
532
573
  controller.enqueue({
@@ -564,6 +605,740 @@ var GitLabAgenticLanguageModel = class {
564
605
  }
565
606
  };
566
607
 
608
+ // src/gitlab-openai-language-model.ts
609
+ import OpenAI from "openai";
610
+
611
+ // src/model-mappings.ts
612
+ var MODEL_MAPPINGS = {
613
+ // Anthropic models
614
+ "duo-chat-opus-4-5": { provider: "anthropic", model: "claude-opus-4-5-20251101" },
615
+ "duo-chat-sonnet-4-5": { provider: "anthropic", model: "claude-sonnet-4-5-20250929" },
616
+ "duo-chat-haiku-4-5": { provider: "anthropic", model: "claude-haiku-4-5-20251001" },
617
+ // OpenAI models - Chat Completions API
618
+ "duo-chat-gpt-5-1": { provider: "openai", model: "gpt-5.1-2025-11-13", openaiApiType: "chat" },
619
+ "duo-chat-gpt-5-mini": {
620
+ provider: "openai",
621
+ model: "gpt-5-mini-2025-08-07",
622
+ openaiApiType: "chat"
623
+ },
624
+ // OpenAI models - Responses API (Codex models)
625
+ "duo-chat-gpt-5-codex": { provider: "openai", model: "gpt-5-codex", openaiApiType: "responses" },
626
+ "duo-chat-gpt-5-2-codex": {
627
+ provider: "openai",
628
+ model: "gpt-5.2-codex",
629
+ openaiApiType: "responses"
630
+ }
631
+ };
632
+ function getModelMapping(modelId) {
633
+ return MODEL_MAPPINGS[modelId];
634
+ }
635
+ function getProviderForModelId(modelId) {
636
+ return MODEL_MAPPINGS[modelId]?.provider;
637
+ }
638
+ function getValidModelsForProvider(provider) {
639
+ return Object.values(MODEL_MAPPINGS).filter((m) => m.provider === provider).map((m) => m.model);
640
+ }
641
+ function getAnthropicModelForModelId(modelId) {
642
+ const mapping = MODEL_MAPPINGS[modelId];
643
+ return mapping?.provider === "anthropic" ? mapping.model : void 0;
644
+ }
645
+ function getOpenAIModelForModelId(modelId) {
646
+ const mapping = MODEL_MAPPINGS[modelId];
647
+ return mapping?.provider === "openai" ? mapping.model : void 0;
648
+ }
649
+ function getOpenAIApiType(modelId) {
650
+ const mapping = MODEL_MAPPINGS[modelId];
651
+ return mapping?.openaiApiType ?? "chat";
652
+ }
653
+ function isResponsesApiModel(modelId) {
654
+ return getOpenAIApiType(modelId) === "responses";
655
+ }
656
+ var MODEL_ID_TO_ANTHROPIC_MODEL = Object.fromEntries(
657
+ Object.entries(MODEL_MAPPINGS).filter(([, v]) => v.provider === "anthropic").map(([k, v]) => [k, v.model])
658
+ );
659
+
660
+ // src/gitlab-openai-language-model.ts
661
+ var GitLabOpenAILanguageModel = class {
662
+ specificationVersion = "v2";
663
+ modelId;
664
+ supportedUrls = {};
665
+ config;
666
+ directAccessClient;
667
+ useResponsesApi;
668
+ openaiClient = null;
669
+ constructor(modelId, config) {
670
+ this.modelId = modelId;
671
+ this.config = config;
672
+ this.useResponsesApi = config.useResponsesApi ?? isResponsesApiModel(modelId);
673
+ this.directAccessClient = new GitLabDirectAccessClient({
674
+ instanceUrl: config.instanceUrl,
675
+ getHeaders: config.getHeaders,
676
+ refreshApiKey: config.refreshApiKey,
677
+ fetch: config.fetch,
678
+ featureFlags: config.featureFlags,
679
+ aiGatewayUrl: config.aiGatewayUrl
680
+ });
681
+ }
682
+ get provider() {
683
+ return this.config.provider;
684
+ }
685
+ async getOpenAIClient(forceRefresh = false) {
686
+ const tokenData = await this.directAccessClient.getDirectAccessToken(forceRefresh);
687
+ const { "x-api-key": _removed, ...filteredHeaders } = tokenData.headers;
688
+ this.openaiClient = new OpenAI({
689
+ apiKey: tokenData.token,
690
+ baseURL: this.directAccessClient.getOpenAIProxyUrl(),
691
+ defaultHeaders: filteredHeaders
692
+ });
693
+ return this.openaiClient;
694
+ }
695
+ isTokenError(error) {
696
+ if (error instanceof OpenAI.APIError) {
697
+ if (error.status === 401) {
698
+ return true;
699
+ }
700
+ const message = error.message?.toLowerCase() || "";
701
+ if (message.includes("token") && (message.includes("expired") || message.includes("revoked") || message.includes("invalid"))) {
702
+ return true;
703
+ }
704
+ }
705
+ return false;
706
+ }
707
+ convertTools(tools) {
708
+ if (!tools || tools.length === 0) {
709
+ return void 0;
710
+ }
711
+ return tools.filter((tool) => tool.type === "function").map((tool) => {
712
+ const schema = tool.inputSchema;
713
+ return {
714
+ type: "function",
715
+ function: {
716
+ name: tool.name,
717
+ description: tool.description || "",
718
+ // Ensure the schema has type: 'object' as OpenAI requires it
719
+ parameters: {
720
+ type: "object",
721
+ ...schema
722
+ }
723
+ }
724
+ };
725
+ });
726
+ }
727
+ convertToolChoice(toolChoice) {
728
+ if (!toolChoice) {
729
+ return void 0;
730
+ }
731
+ switch (toolChoice.type) {
732
+ case "auto":
733
+ return "auto";
734
+ case "none":
735
+ return "none";
736
+ case "required":
737
+ return "required";
738
+ case "tool":
739
+ return { type: "function", function: { name: toolChoice.toolName } };
740
+ default:
741
+ return void 0;
742
+ }
743
+ }
744
+ convertPrompt(prompt) {
745
+ const messages = [];
746
+ for (const message of prompt) {
747
+ if (message.role === "system") {
748
+ messages.push({ role: "system", content: message.content });
749
+ continue;
750
+ }
751
+ if (message.role === "user") {
752
+ const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text);
753
+ if (textParts.length > 0) {
754
+ messages.push({ role: "user", content: textParts.join("\n") });
755
+ }
756
+ } else if (message.role === "assistant") {
757
+ const textParts = [];
758
+ const toolCalls = [];
759
+ for (const part of message.content) {
760
+ if (part.type === "text") {
761
+ textParts.push(part.text);
762
+ } else if (part.type === "tool-call") {
763
+ toolCalls.push({
764
+ id: part.toolCallId,
765
+ type: "function",
766
+ function: {
767
+ name: part.toolName,
768
+ arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input)
769
+ }
770
+ });
771
+ }
772
+ }
773
+ const assistantMessage = {
774
+ role: "assistant",
775
+ content: textParts.length > 0 ? textParts.join("\n") : null
776
+ };
777
+ if (toolCalls.length > 0) {
778
+ assistantMessage.tool_calls = toolCalls;
779
+ }
780
+ messages.push(assistantMessage);
781
+ } else if (message.role === "tool") {
782
+ for (const part of message.content) {
783
+ if (part.type === "tool-result") {
784
+ let resultContent;
785
+ if (part.output.type === "text") {
786
+ resultContent = part.output.value;
787
+ } else if (part.output.type === "json") {
788
+ resultContent = JSON.stringify(part.output.value);
789
+ } else if (part.output.type === "error-text") {
790
+ resultContent = part.output.value;
791
+ } else if (part.output.type === "error-json") {
792
+ resultContent = JSON.stringify(part.output.value);
793
+ } else {
794
+ resultContent = JSON.stringify(part.output);
795
+ }
796
+ messages.push({
797
+ role: "tool",
798
+ tool_call_id: part.toolCallId,
799
+ content: resultContent
800
+ });
801
+ }
802
+ }
803
+ }
804
+ }
805
+ return messages;
806
+ }
807
+ convertFinishReason(finishReason) {
808
+ switch (finishReason) {
809
+ case "stop":
810
+ return "stop";
811
+ case "length":
812
+ return "length";
813
+ case "tool_calls":
814
+ return "tool-calls";
815
+ case "content_filter":
816
+ return "content-filter";
817
+ default:
818
+ return "unknown";
819
+ }
820
+ }
821
+ /**
822
+ * Convert tools to Responses API format
823
+ */
824
+ convertToolsForResponses(tools) {
825
+ if (!tools || tools.length === 0) {
826
+ return void 0;
827
+ }
828
+ return tools.filter((tool) => tool.type === "function").map((tool) => {
829
+ const schema = { ...tool.inputSchema };
830
+ delete schema["$schema"];
831
+ return {
832
+ type: "function",
833
+ name: tool.name,
834
+ description: tool.description || "",
835
+ parameters: schema,
836
+ strict: false
837
+ };
838
+ });
839
+ }
840
+ /**
841
+ * Convert prompt to Responses API input format
842
+ */
843
+ convertPromptForResponses(prompt) {
844
+ const items = [];
845
+ for (const message of prompt) {
846
+ if (message.role === "system") {
847
+ continue;
848
+ }
849
+ if (message.role === "user") {
850
+ const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text);
851
+ if (textParts.length > 0) {
852
+ items.push({
853
+ type: "message",
854
+ role: "user",
855
+ content: textParts.map((text) => ({ type: "input_text", text }))
856
+ });
857
+ }
858
+ } else if (message.role === "assistant") {
859
+ const textParts = [];
860
+ for (const part of message.content) {
861
+ if (part.type === "text") {
862
+ textParts.push(part.text);
863
+ } else if (part.type === "tool-call") {
864
+ items.push({
865
+ type: "function_call",
866
+ call_id: part.toolCallId,
867
+ name: part.toolName,
868
+ arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input)
869
+ });
870
+ }
871
+ }
872
+ if (textParts.length > 0) {
873
+ items.push({
874
+ type: "message",
875
+ role: "assistant",
876
+ content: [{ type: "output_text", text: textParts.join("\n"), annotations: [] }]
877
+ });
878
+ }
879
+ } else if (message.role === "tool") {
880
+ for (const part of message.content) {
881
+ if (part.type === "tool-result") {
882
+ let resultContent;
883
+ if (part.output.type === "text") {
884
+ resultContent = part.output.value;
885
+ } else if (part.output.type === "json") {
886
+ resultContent = JSON.stringify(part.output.value);
887
+ } else if (part.output.type === "error-text") {
888
+ resultContent = part.output.value;
889
+ } else if (part.output.type === "error-json") {
890
+ resultContent = JSON.stringify(part.output.value);
891
+ } else {
892
+ resultContent = JSON.stringify(part.output);
893
+ }
894
+ items.push({
895
+ type: "function_call_output",
896
+ call_id: part.toolCallId,
897
+ output: resultContent
898
+ });
899
+ }
900
+ }
901
+ }
902
+ }
903
+ return items;
904
+ }
905
+ /**
906
+ * Extract system instructions from prompt
907
+ */
908
+ extractSystemInstructions(prompt) {
909
+ const systemMessages = prompt.filter((m) => m.role === "system").map((m) => m.content).join("\n");
910
+ return systemMessages || void 0;
911
+ }
912
+ /**
913
+ * Convert Responses API status to finish reason
914
+ * Note: Responses API returns 'completed' even when making tool calls,
915
+ * so we need to check the content for tool calls separately.
916
+ */
917
+ convertResponsesStatus(status, hasToolCalls = false) {
918
+ if (hasToolCalls) {
919
+ return "tool-calls";
920
+ }
921
+ switch (status) {
922
+ case "completed":
923
+ return "stop";
924
+ case "incomplete":
925
+ return "length";
926
+ case "cancelled":
927
+ return "stop";
928
+ case "failed":
929
+ return "error";
930
+ default:
931
+ return "unknown";
932
+ }
933
+ }
934
+ async doGenerate(options) {
935
+ if (this.useResponsesApi) {
936
+ return this.doGenerateWithResponsesApi(options, false);
937
+ }
938
+ return this.doGenerateWithChatApi(options, false);
939
+ }
940
+ async doGenerateWithChatApi(options, isRetry) {
941
+ const client = await this.getOpenAIClient(isRetry);
942
+ const messages = this.convertPrompt(options.prompt);
943
+ const tools = this.convertTools(options.tools);
944
+ const toolChoice = options.toolChoice?.type !== "none" ? this.convertToolChoice(options.toolChoice) : void 0;
945
+ const openaiModel = this.config.openaiModel || "gpt-4o";
946
+ const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
947
+ try {
948
+ const response = await client.chat.completions.create({
949
+ model: openaiModel,
950
+ max_completion_tokens: maxTokens,
951
+ messages,
952
+ tools,
953
+ tool_choice: tools ? toolChoice : void 0,
954
+ temperature: options.temperature,
955
+ top_p: options.topP,
956
+ stop: options.stopSequences
957
+ });
958
+ const choice = response.choices[0];
959
+ const content = [];
960
+ if (choice?.message.content) {
961
+ content.push({ type: "text", text: choice.message.content });
962
+ }
963
+ if (choice?.message.tool_calls) {
964
+ for (const toolCall of choice.message.tool_calls) {
965
+ if (toolCall.type === "function") {
966
+ content.push({
967
+ type: "tool-call",
968
+ toolCallId: toolCall.id,
969
+ toolName: toolCall.function.name,
970
+ input: toolCall.function.arguments
971
+ });
972
+ }
973
+ }
974
+ }
975
+ const usage = {
976
+ inputTokens: response.usage?.prompt_tokens || 0,
977
+ outputTokens: response.usage?.completion_tokens || 0,
978
+ totalTokens: response.usage?.total_tokens || 0
979
+ };
980
+ return {
981
+ content,
982
+ finishReason: this.convertFinishReason(choice?.finish_reason),
983
+ usage,
984
+ warnings: []
985
+ };
986
+ } catch (error) {
987
+ if (!isRetry && this.isTokenError(error)) {
988
+ this.directAccessClient.invalidateToken();
989
+ return this.doGenerateWithChatApi(options, true);
990
+ }
991
+ if (error instanceof OpenAI.APIError) {
992
+ throw new GitLabError({
993
+ message: `OpenAI API error: ${error.message}`,
994
+ cause: error
995
+ });
996
+ }
997
+ throw error;
998
+ }
999
+ }
1000
+ async doGenerateWithResponsesApi(options, isRetry) {
1001
+ const client = await this.getOpenAIClient(isRetry);
1002
+ const input = this.convertPromptForResponses(options.prompt);
1003
+ const tools = this.convertToolsForResponses(options.tools);
1004
+ const instructions = this.extractSystemInstructions(options.prompt);
1005
+ const openaiModel = this.config.openaiModel || "gpt-5-codex";
1006
+ const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
1007
+ try {
1008
+ const response = await client.responses.create({
1009
+ model: openaiModel,
1010
+ input,
1011
+ instructions,
1012
+ tools,
1013
+ max_output_tokens: maxTokens,
1014
+ temperature: options.temperature,
1015
+ top_p: options.topP,
1016
+ store: false
1017
+ });
1018
+ const content = [];
1019
+ let hasToolCalls = false;
1020
+ for (const item of response.output || []) {
1021
+ if (item.type === "message" && item.role === "assistant") {
1022
+ for (const contentItem of item.content || []) {
1023
+ if (contentItem.type === "output_text") {
1024
+ content.push({ type: "text", text: contentItem.text });
1025
+ }
1026
+ }
1027
+ } else if (item.type === "function_call") {
1028
+ hasToolCalls = true;
1029
+ content.push({
1030
+ type: "tool-call",
1031
+ toolCallId: item.call_id,
1032
+ toolName: item.name,
1033
+ input: item.arguments
1034
+ });
1035
+ }
1036
+ }
1037
+ const usage = {
1038
+ inputTokens: response.usage?.input_tokens || 0,
1039
+ outputTokens: response.usage?.output_tokens || 0,
1040
+ totalTokens: response.usage?.total_tokens || 0
1041
+ };
1042
+ return {
1043
+ content,
1044
+ finishReason: this.convertResponsesStatus(response.status, hasToolCalls),
1045
+ usage,
1046
+ warnings: []
1047
+ };
1048
+ } catch (error) {
1049
+ if (!isRetry && this.isTokenError(error)) {
1050
+ this.directAccessClient.invalidateToken();
1051
+ return this.doGenerateWithResponsesApi(options, true);
1052
+ }
1053
+ if (error instanceof OpenAI.APIError) {
1054
+ throw new GitLabError({
1055
+ message: `OpenAI API error: ${error.message}`,
1056
+ cause: error
1057
+ });
1058
+ }
1059
+ throw error;
1060
+ }
1061
+ }
1062
+ async doStream(options) {
1063
+ if (this.useResponsesApi) {
1064
+ return this.doStreamWithResponsesApi(options, false);
1065
+ }
1066
+ return this.doStreamWithChatApi(options, false);
1067
+ }
1068
+ async doStreamWithChatApi(options, isRetry) {
1069
+ const client = await this.getOpenAIClient(isRetry);
1070
+ const messages = this.convertPrompt(options.prompt);
1071
+ const tools = this.convertTools(options.tools);
1072
+ const toolChoice = options.toolChoice?.type !== "none" ? this.convertToolChoice(options.toolChoice) : void 0;
1073
+ const openaiModel = this.config.openaiModel || "gpt-4o";
1074
+ const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
1075
+ const requestBody = {
1076
+ model: openaiModel,
1077
+ max_completion_tokens: maxTokens,
1078
+ messages,
1079
+ tools,
1080
+ tool_choice: tools ? toolChoice : void 0,
1081
+ temperature: options.temperature,
1082
+ top_p: options.topP,
1083
+ stop: options.stopSequences,
1084
+ stream: true,
1085
+ stream_options: { include_usage: true }
1086
+ };
1087
+ const self = this;
1088
+ const stream = new ReadableStream({
1089
+ start: async (controller) => {
1090
+ const toolCalls = {};
1091
+ const usage = {
1092
+ inputTokens: 0,
1093
+ outputTokens: 0,
1094
+ totalTokens: 0
1095
+ };
1096
+ let finishReason = "unknown";
1097
+ let textStarted = false;
1098
+ const textId = "text-0";
1099
+ try {
1100
+ const openaiStream = await client.chat.completions.create({
1101
+ ...requestBody,
1102
+ stream: true
1103
+ });
1104
+ controller.enqueue({ type: "stream-start", warnings: [] });
1105
+ for await (const chunk of openaiStream) {
1106
+ const choice = chunk.choices?.[0];
1107
+ if (chunk.id && !textStarted) {
1108
+ controller.enqueue({
1109
+ type: "response-metadata",
1110
+ id: chunk.id,
1111
+ modelId: chunk.model
1112
+ });
1113
+ }
1114
+ if (choice?.delta?.content) {
1115
+ if (!textStarted) {
1116
+ controller.enqueue({ type: "text-start", id: textId });
1117
+ textStarted = true;
1118
+ }
1119
+ controller.enqueue({
1120
+ type: "text-delta",
1121
+ id: textId,
1122
+ delta: choice.delta.content
1123
+ });
1124
+ }
1125
+ if (choice?.delta?.tool_calls) {
1126
+ for (const tc of choice.delta.tool_calls) {
1127
+ const idx = tc.index;
1128
+ if (!toolCalls[idx]) {
1129
+ toolCalls[idx] = {
1130
+ id: tc.id || "",
1131
+ name: tc.function?.name || "",
1132
+ arguments: ""
1133
+ };
1134
+ controller.enqueue({
1135
+ type: "tool-input-start",
1136
+ id: toolCalls[idx].id,
1137
+ toolName: toolCalls[idx].name
1138
+ });
1139
+ }
1140
+ if (tc.function?.arguments) {
1141
+ toolCalls[idx].arguments += tc.function.arguments;
1142
+ controller.enqueue({
1143
+ type: "tool-input-delta",
1144
+ id: toolCalls[idx].id,
1145
+ delta: tc.function.arguments
1146
+ });
1147
+ }
1148
+ }
1149
+ }
1150
+ if (choice?.finish_reason) {
1151
+ finishReason = self.convertFinishReason(choice.finish_reason);
1152
+ }
1153
+ if (chunk.usage) {
1154
+ usage.inputTokens = chunk.usage.prompt_tokens || 0;
1155
+ usage.outputTokens = chunk.usage.completion_tokens || 0;
1156
+ usage.totalTokens = chunk.usage.total_tokens || 0;
1157
+ }
1158
+ }
1159
+ if (textStarted) {
1160
+ controller.enqueue({ type: "text-end", id: textId });
1161
+ }
1162
+ for (const [, tc] of Object.entries(toolCalls)) {
1163
+ controller.enqueue({ type: "tool-input-end", id: tc.id });
1164
+ controller.enqueue({
1165
+ type: "tool-call",
1166
+ toolCallId: tc.id,
1167
+ toolName: tc.name,
1168
+ input: tc.arguments || "{}"
1169
+ });
1170
+ }
1171
+ controller.enqueue({ type: "finish", finishReason, usage });
1172
+ controller.close();
1173
+ } catch (error) {
1174
+ if (!isRetry && self.isTokenError(error)) {
1175
+ self.directAccessClient.invalidateToken();
1176
+ controller.enqueue({
1177
+ type: "error",
1178
+ error: new GitLabError({ message: "TOKEN_REFRESH_NEEDED", cause: error })
1179
+ });
1180
+ controller.close();
1181
+ return;
1182
+ }
1183
+ if (error instanceof OpenAI.APIError) {
1184
+ controller.enqueue({
1185
+ type: "error",
1186
+ error: new GitLabError({
1187
+ message: `OpenAI API error: ${error.message}`,
1188
+ cause: error
1189
+ })
1190
+ });
1191
+ } else {
1192
+ controller.enqueue({ type: "error", error });
1193
+ }
1194
+ controller.close();
1195
+ }
1196
+ }
1197
+ });
1198
+ return { stream, request: { body: requestBody } };
1199
+ }
1200
+ async doStreamWithResponsesApi(options, isRetry) {
1201
+ const client = await this.getOpenAIClient(isRetry);
1202
+ const input = this.convertPromptForResponses(options.prompt);
1203
+ const tools = this.convertToolsForResponses(options.tools);
1204
+ const instructions = this.extractSystemInstructions(options.prompt);
1205
+ const openaiModel = this.config.openaiModel || "gpt-5-codex";
1206
+ const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
1207
+ const requestBody = {
1208
+ model: openaiModel,
1209
+ input,
1210
+ instructions,
1211
+ tools,
1212
+ max_output_tokens: maxTokens,
1213
+ temperature: options.temperature,
1214
+ top_p: options.topP,
1215
+ store: false,
1216
+ stream: true
1217
+ };
1218
+ const self = this;
1219
+ const stream = new ReadableStream({
1220
+ start: async (controller) => {
1221
+ const toolCalls = {};
1222
+ const usage = {
1223
+ inputTokens: 0,
1224
+ outputTokens: 0,
1225
+ totalTokens: 0
1226
+ };
1227
+ let finishReason = "unknown";
1228
+ let textStarted = false;
1229
+ const textId = "text-0";
1230
+ try {
1231
+ const openaiStream = await client.responses.create({
1232
+ ...requestBody,
1233
+ stream: true
1234
+ });
1235
+ controller.enqueue({ type: "stream-start", warnings: [] });
1236
+ for await (const event of openaiStream) {
1237
+ if (event.type === "response.created") {
1238
+ controller.enqueue({
1239
+ type: "response-metadata",
1240
+ id: event.response.id,
1241
+ modelId: event.response.model
1242
+ });
1243
+ } else if (event.type === "response.output_item.added") {
1244
+ if (event.item.type === "function_call") {
1245
+ const outputIndex = event.output_index;
1246
+ const callId = event.item.call_id;
1247
+ toolCalls[outputIndex] = {
1248
+ callId,
1249
+ name: event.item.name,
1250
+ arguments: ""
1251
+ };
1252
+ controller.enqueue({
1253
+ type: "tool-input-start",
1254
+ id: callId,
1255
+ toolName: event.item.name
1256
+ });
1257
+ }
1258
+ } else if (event.type === "response.output_text.delta") {
1259
+ if (!textStarted) {
1260
+ controller.enqueue({ type: "text-start", id: textId });
1261
+ textStarted = true;
1262
+ }
1263
+ controller.enqueue({
1264
+ type: "text-delta",
1265
+ id: textId,
1266
+ delta: event.delta
1267
+ });
1268
+ } else if (event.type === "response.function_call_arguments.delta") {
1269
+ const outputIndex = event.output_index;
1270
+ const tc = toolCalls[outputIndex];
1271
+ if (tc) {
1272
+ tc.arguments += event.delta;
1273
+ controller.enqueue({
1274
+ type: "tool-input-delta",
1275
+ id: tc.callId,
1276
+ delta: event.delta
1277
+ });
1278
+ }
1279
+ } else if (event.type === "response.function_call_arguments.done") {
1280
+ const outputIndex = event.output_index;
1281
+ const tc = toolCalls[outputIndex];
1282
+ if (tc) {
1283
+ tc.arguments = event.arguments;
1284
+ }
1285
+ } else if (event.type === "response.completed") {
1286
+ const hasToolCalls2 = Object.keys(toolCalls).length > 0;
1287
+ finishReason = self.convertResponsesStatus(event.response.status, hasToolCalls2);
1288
+ if (event.response.usage) {
1289
+ usage.inputTokens = event.response.usage.input_tokens || 0;
1290
+ usage.outputTokens = event.response.usage.output_tokens || 0;
1291
+ usage.totalTokens = event.response.usage.total_tokens || 0;
1292
+ }
1293
+ }
1294
+ }
1295
+ if (textStarted) {
1296
+ controller.enqueue({ type: "text-end", id: textId });
1297
+ }
1298
+ const hasToolCalls = Object.keys(toolCalls).length > 0;
1299
+ if (hasToolCalls && finishReason === "stop") {
1300
+ finishReason = "tool-calls";
1301
+ }
1302
+ for (const tc of Object.values(toolCalls)) {
1303
+ controller.enqueue({ type: "tool-input-end", id: tc.callId });
1304
+ controller.enqueue({
1305
+ type: "tool-call",
1306
+ toolCallId: tc.callId,
1307
+ toolName: tc.name,
1308
+ input: tc.arguments || "{}"
1309
+ });
1310
+ }
1311
+ controller.enqueue({ type: "finish", finishReason, usage });
1312
+ controller.close();
1313
+ } catch (error) {
1314
+ if (!isRetry && self.isTokenError(error)) {
1315
+ self.directAccessClient.invalidateToken();
1316
+ controller.enqueue({
1317
+ type: "error",
1318
+ error: new GitLabError({ message: "TOKEN_REFRESH_NEEDED", cause: error })
1319
+ });
1320
+ controller.close();
1321
+ return;
1322
+ }
1323
+ if (error instanceof OpenAI.APIError) {
1324
+ controller.enqueue({
1325
+ type: "error",
1326
+ error: new GitLabError({
1327
+ message: `OpenAI API error: ${error.message}`,
1328
+ cause: error
1329
+ })
1330
+ });
1331
+ } else {
1332
+ controller.enqueue({ type: "error", error });
1333
+ }
1334
+ controller.close();
1335
+ }
1336
+ }
1337
+ });
1338
+ return { stream, request: { body: requestBody } };
1339
+ }
1340
+ };
1341
+
567
1342
  // src/gitlab-oauth-types.ts
568
1343
  var BUNDLED_CLIENT_ID = "36f2a70cddeb5a0889d4fd8295c241b7e9848e89cf9e599d0eed2d8e5350fbf5";
569
1344
  var GITLAB_COM_URL = "https://gitlab.com";
@@ -726,16 +1501,6 @@ var GitLabOAuthManager = class {
726
1501
  }
727
1502
  };
728
1503
 
729
- // src/model-mappings.ts
730
- var MODEL_ID_TO_ANTHROPIC_MODEL = {
731
- "duo-chat-opus-4-5": "claude-opus-4-5-20251101",
732
- "duo-chat-sonnet-4-5": "claude-sonnet-4-5-20250929",
733
- "duo-chat-haiku-4-5": "claude-haiku-4-5-20251001"
734
- };
735
- function getAnthropicModelForModelId(modelId) {
736
- return MODEL_ID_TO_ANTHROPIC_MODEL[modelId];
737
- }
738
-
739
1504
  // src/gitlab-provider.ts
740
1505
  import * as fs from "fs";
741
1506
  import * as path from "path";
@@ -871,21 +1636,44 @@ function createGitLab(options = {}) {
871
1636
  getApiKey().catch(() => {
872
1637
  });
873
1638
  const createAgenticChatModel = (modelId, agenticOptions) => {
1639
+ const mapping = getModelMapping(modelId);
1640
+ if (!mapping) {
1641
+ throw new GitLabError({
1642
+ message: `Unknown model ID: ${modelId}. Model must be registered in MODEL_MAPPINGS.`
1643
+ });
1644
+ }
1645
+ if (agenticOptions?.providerModel) {
1646
+ const validModels = getValidModelsForProvider(mapping.provider);
1647
+ if (!validModels.includes(agenticOptions.providerModel)) {
1648
+ throw new GitLabError({
1649
+ message: `Invalid providerModel '${agenticOptions.providerModel}' for provider '${mapping.provider}'. Valid models: ${validModels.join(", ")}`
1650
+ });
1651
+ }
1652
+ }
874
1653
  const featureFlags = {
875
1654
  DuoAgentPlatformNext: true,
876
1655
  ...options.featureFlags,
877
1656
  ...agenticOptions?.featureFlags
878
1657
  };
879
- return new GitLabAgenticLanguageModel(modelId, {
1658
+ const baseConfig = {
880
1659
  provider: `${providerName}.agentic`,
881
1660
  instanceUrl,
882
1661
  getHeaders,
883
1662
  refreshApiKey,
884
1663
  fetch: options.fetch,
885
- anthropicModel: agenticOptions?.anthropicModel ?? getAnthropicModelForModelId(modelId),
886
1664
  maxTokens: agenticOptions?.maxTokens,
887
1665
  featureFlags,
888
1666
  aiGatewayUrl: options.aiGatewayUrl
1667
+ };
1668
+ if (mapping.provider === "openai") {
1669
+ return new GitLabOpenAILanguageModel(modelId, {
1670
+ ...baseConfig,
1671
+ openaiModel: agenticOptions?.providerModel ?? mapping.model
1672
+ });
1673
+ }
1674
+ return new GitLabAnthropicLanguageModel(modelId, {
1675
+ ...baseConfig,
1676
+ anthropicModel: agenticOptions?.providerModel ?? mapping.model
889
1677
  });
890
1678
  };
891
1679
  const createDefaultModel = (modelId) => {
@@ -996,7 +1784,7 @@ var GitLabProjectCache = class {
996
1784
  };
997
1785
 
998
1786
  // src/gitlab-project-detector.ts
999
- var debugLog2 = (..._args) => {
1787
+ var debugLog = (..._args) => {
1000
1788
  };
1001
1789
  var GitLabProjectDetector = class {
1002
1790
  config;
@@ -1025,35 +1813,35 @@ var GitLabProjectDetector = class {
1025
1813
  return cached;
1026
1814
  }
1027
1815
  try {
1028
- debugLog2(`[GitLabProjectDetector] Getting git remote URL from: ${workingDirectory}`);
1816
+ debugLog(`[GitLabProjectDetector] Getting git remote URL from: ${workingDirectory}`);
1029
1817
  const remoteUrl = await this.getGitRemoteUrl(workingDirectory, remoteName);
1030
1818
  if (!remoteUrl) {
1031
- debugLog2(`[GitLabProjectDetector] No git remote URL found`);
1819
+ debugLog(`[GitLabProjectDetector] No git remote URL found`);
1032
1820
  return null;
1033
1821
  }
1034
- debugLog2(`[GitLabProjectDetector] Git remote URL: ${remoteUrl}`);
1035
- debugLog2(
1822
+ debugLog(`[GitLabProjectDetector] Git remote URL: ${remoteUrl}`);
1823
+ debugLog(
1036
1824
  `[GitLabProjectDetector] Parsing project path from URL (instance: ${this.config.instanceUrl})`
1037
1825
  );
1038
1826
  const projectPath = this.parseGitRemoteUrl(remoteUrl, this.config.instanceUrl);
1039
1827
  if (!projectPath) {
1040
- debugLog2(
1828
+ debugLog(
1041
1829
  `[GitLabProjectDetector] Could not parse project path from URL (remote doesn't match instance)`
1042
1830
  );
1043
1831
  return null;
1044
1832
  }
1045
- debugLog2(`[GitLabProjectDetector] Parsed project path: ${projectPath}`);
1046
- debugLog2(`[GitLabProjectDetector] Fetching project from GitLab API: ${projectPath}`);
1833
+ debugLog(`[GitLabProjectDetector] Parsed project path: ${projectPath}`);
1834
+ debugLog(`[GitLabProjectDetector] Fetching project from GitLab API: ${projectPath}`);
1047
1835
  const project = await this.getProjectByPath(projectPath);
1048
- debugLog2(`[GitLabProjectDetector] \u2713 Project fetched successfully:`, project);
1836
+ debugLog(`[GitLabProjectDetector] \u2713 Project fetched successfully:`, project);
1049
1837
  this.cache.set(cacheKey, project);
1050
1838
  return project;
1051
1839
  } catch (error) {
1052
1840
  if (error instanceof GitLabError) {
1053
- debugLog2(`[GitLabProjectDetector] GitLab API error:`, error.message || error);
1841
+ debugLog(`[GitLabProjectDetector] GitLab API error:`, error.message || error);
1054
1842
  return null;
1055
1843
  }
1056
- debugLog2(`[GitLabProjectDetector] Unexpected error:`, error);
1844
+ debugLog(`[GitLabProjectDetector] Unexpected error:`, error);
1057
1845
  console.warn(`Failed to auto-detect GitLab project: ${error}`);
1058
1846
  return null;
1059
1847
  }
@@ -1184,17 +1972,25 @@ export {
1184
1972
  BUNDLED_CLIENT_ID,
1185
1973
  DEFAULT_AI_GATEWAY_URL,
1186
1974
  GITLAB_COM_URL,
1187
- GitLabAgenticLanguageModel,
1975
+ GitLabAnthropicLanguageModel,
1188
1976
  GitLabDirectAccessClient,
1189
1977
  GitLabError,
1190
1978
  GitLabOAuthManager,
1979
+ GitLabOpenAILanguageModel,
1191
1980
  GitLabProjectCache,
1192
1981
  GitLabProjectDetector,
1193
1982
  MODEL_ID_TO_ANTHROPIC_MODEL,
1983
+ MODEL_MAPPINGS,
1194
1984
  OAUTH_SCOPES,
1195
1985
  TOKEN_EXPIRY_SKEW_MS,
1196
1986
  createGitLab,
1197
1987
  getAnthropicModelForModelId,
1198
- gitlab
1988
+ getModelMapping,
1989
+ getOpenAIApiType,
1990
+ getOpenAIModelForModelId,
1991
+ getProviderForModelId,
1992
+ getValidModelsForProvider,
1993
+ gitlab,
1994
+ isResponsesApiModel
1199
1995
  };
1200
1996
  //# sourceMappingURL=index.mjs.map