@gitlab/gitlab-ai-provider 3.1.3 → 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,8 +208,8 @@ var GitLabDirectAccessClient = class {
191
208
  }
192
209
  };
193
210
 
194
- // src/gitlab-agentic-language-model.ts
195
- var GitLabAgenticLanguageModel = class {
211
+ // src/gitlab-anthropic-language-model.ts
212
+ var GitLabAnthropicLanguageModel = class {
196
213
  specificationVersion = "v2";
197
214
  modelId;
198
215
  supportedUrls = {};
@@ -645,6 +662,740 @@ var GitLabAgenticLanguageModel = class {
645
662
  }
646
663
  };
647
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
+
648
1399
  // src/gitlab-oauth-types.ts
649
1400
  var BUNDLED_CLIENT_ID = "36f2a70cddeb5a0889d4fd8295c241b7e9848e89cf9e599d0eed2d8e5350fbf5";
650
1401
  var GITLAB_COM_URL = "https://gitlab.com";
@@ -807,16 +1558,6 @@ var GitLabOAuthManager = class {
807
1558
  }
808
1559
  };
809
1560
 
810
- // src/model-mappings.ts
811
- var MODEL_ID_TO_ANTHROPIC_MODEL = {
812
- "duo-chat-opus-4-5": "claude-opus-4-5-20251101",
813
- "duo-chat-sonnet-4-5": "claude-sonnet-4-5-20250929",
814
- "duo-chat-haiku-4-5": "claude-haiku-4-5-20251001"
815
- };
816
- function getAnthropicModelForModelId(modelId) {
817
- return MODEL_ID_TO_ANTHROPIC_MODEL[modelId];
818
- }
819
-
820
1561
  // src/gitlab-provider.ts
821
1562
  var fs = __toESM(require("fs"));
822
1563
  var path = __toESM(require("path"));
@@ -952,21 +1693,44 @@ function createGitLab(options = {}) {
952
1693
  getApiKey().catch(() => {
953
1694
  });
954
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
+ }
955
1710
  const featureFlags = {
956
1711
  DuoAgentPlatformNext: true,
957
1712
  ...options.featureFlags,
958
1713
  ...agenticOptions?.featureFlags
959
1714
  };
960
- return new GitLabAgenticLanguageModel(modelId, {
1715
+ const baseConfig = {
961
1716
  provider: `${providerName}.agentic`,
962
1717
  instanceUrl,
963
1718
  getHeaders,
964
1719
  refreshApiKey,
965
1720
  fetch: options.fetch,
966
- anthropicModel: agenticOptions?.anthropicModel ?? getAnthropicModelForModelId(modelId),
967
1721
  maxTokens: agenticOptions?.maxTokens,
968
1722
  featureFlags,
969
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
970
1734
  });
971
1735
  };
972
1736
  const createDefaultModel = (modelId) => {
@@ -1266,17 +2030,25 @@ var GitLabProjectDetector = class {
1266
2030
  BUNDLED_CLIENT_ID,
1267
2031
  DEFAULT_AI_GATEWAY_URL,
1268
2032
  GITLAB_COM_URL,
1269
- GitLabAgenticLanguageModel,
2033
+ GitLabAnthropicLanguageModel,
1270
2034
  GitLabDirectAccessClient,
1271
2035
  GitLabError,
1272
2036
  GitLabOAuthManager,
2037
+ GitLabOpenAILanguageModel,
1273
2038
  GitLabProjectCache,
1274
2039
  GitLabProjectDetector,
1275
2040
  MODEL_ID_TO_ANTHROPIC_MODEL,
2041
+ MODEL_MAPPINGS,
1276
2042
  OAUTH_SCOPES,
1277
2043
  TOKEN_EXPIRY_SKEW_MS,
1278
2044
  createGitLab,
1279
2045
  getAnthropicModelForModelId,
1280
- gitlab
2046
+ getModelMapping,
2047
+ getOpenAIApiType,
2048
+ getOpenAIModelForModelId,
2049
+ getProviderForModelId,
2050
+ getValidModelsForProvider,
2051
+ gitlab,
2052
+ isResponsesApiModel
1281
2053
  });
1282
2054
  //# sourceMappingURL=index.js.map