@gitlab/gitlab-ai-provider 3.1.3 → 3.3.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,741 @@ 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-2": { provider: "openai", model: "gpt-5.2-2025-12-11", openaiApiType: "chat" },
677
+ "duo-chat-gpt-5-mini": {
678
+ provider: "openai",
679
+ model: "gpt-5-mini-2025-08-07",
680
+ openaiApiType: "chat"
681
+ },
682
+ // OpenAI models - Responses API (Codex models)
683
+ "duo-chat-gpt-5-codex": { provider: "openai", model: "gpt-5-codex", openaiApiType: "responses" },
684
+ "duo-chat-gpt-5-2-codex": {
685
+ provider: "openai",
686
+ model: "gpt-5.2-codex",
687
+ openaiApiType: "responses"
688
+ }
689
+ };
690
+ function getModelMapping(modelId) {
691
+ return MODEL_MAPPINGS[modelId];
692
+ }
693
+ function getProviderForModelId(modelId) {
694
+ return MODEL_MAPPINGS[modelId]?.provider;
695
+ }
696
+ function getValidModelsForProvider(provider) {
697
+ return Object.values(MODEL_MAPPINGS).filter((m) => m.provider === provider).map((m) => m.model);
698
+ }
699
+ function getAnthropicModelForModelId(modelId) {
700
+ const mapping = MODEL_MAPPINGS[modelId];
701
+ return mapping?.provider === "anthropic" ? mapping.model : void 0;
702
+ }
703
+ function getOpenAIModelForModelId(modelId) {
704
+ const mapping = MODEL_MAPPINGS[modelId];
705
+ return mapping?.provider === "openai" ? mapping.model : void 0;
706
+ }
707
+ function getOpenAIApiType(modelId) {
708
+ const mapping = MODEL_MAPPINGS[modelId];
709
+ return mapping?.openaiApiType ?? "chat";
710
+ }
711
+ function isResponsesApiModel(modelId) {
712
+ return getOpenAIApiType(modelId) === "responses";
713
+ }
714
+ var MODEL_ID_TO_ANTHROPIC_MODEL = Object.fromEntries(
715
+ Object.entries(MODEL_MAPPINGS).filter(([, v]) => v.provider === "anthropic").map(([k, v]) => [k, v.model])
716
+ );
717
+
718
+ // src/gitlab-openai-language-model.ts
719
+ var GitLabOpenAILanguageModel = class {
720
+ specificationVersion = "v2";
721
+ modelId;
722
+ supportedUrls = {};
723
+ config;
724
+ directAccessClient;
725
+ useResponsesApi;
726
+ openaiClient = null;
727
+ constructor(modelId, config) {
728
+ this.modelId = modelId;
729
+ this.config = config;
730
+ this.useResponsesApi = config.useResponsesApi ?? isResponsesApiModel(modelId);
731
+ this.directAccessClient = new GitLabDirectAccessClient({
732
+ instanceUrl: config.instanceUrl,
733
+ getHeaders: config.getHeaders,
734
+ refreshApiKey: config.refreshApiKey,
735
+ fetch: config.fetch,
736
+ featureFlags: config.featureFlags,
737
+ aiGatewayUrl: config.aiGatewayUrl
738
+ });
739
+ }
740
+ get provider() {
741
+ return this.config.provider;
742
+ }
743
+ async getOpenAIClient(forceRefresh = false) {
744
+ const tokenData = await this.directAccessClient.getDirectAccessToken(forceRefresh);
745
+ const { "x-api-key": _removed, ...filteredHeaders } = tokenData.headers;
746
+ this.openaiClient = new import_openai.default({
747
+ apiKey: tokenData.token,
748
+ baseURL: this.directAccessClient.getOpenAIProxyUrl(),
749
+ defaultHeaders: filteredHeaders
750
+ });
751
+ return this.openaiClient;
752
+ }
753
+ isTokenError(error) {
754
+ if (error instanceof import_openai.default.APIError) {
755
+ if (error.status === 401) {
756
+ return true;
757
+ }
758
+ const message = error.message?.toLowerCase() || "";
759
+ if (message.includes("token") && (message.includes("expired") || message.includes("revoked") || message.includes("invalid"))) {
760
+ return true;
761
+ }
762
+ }
763
+ return false;
764
+ }
765
+ convertTools(tools) {
766
+ if (!tools || tools.length === 0) {
767
+ return void 0;
768
+ }
769
+ return tools.filter((tool) => tool.type === "function").map((tool) => {
770
+ const schema = tool.inputSchema;
771
+ return {
772
+ type: "function",
773
+ function: {
774
+ name: tool.name,
775
+ description: tool.description || "",
776
+ // Ensure the schema has type: 'object' as OpenAI requires it
777
+ parameters: {
778
+ type: "object",
779
+ ...schema
780
+ }
781
+ }
782
+ };
783
+ });
784
+ }
785
+ convertToolChoice(toolChoice) {
786
+ if (!toolChoice) {
787
+ return void 0;
788
+ }
789
+ switch (toolChoice.type) {
790
+ case "auto":
791
+ return "auto";
792
+ case "none":
793
+ return "none";
794
+ case "required":
795
+ return "required";
796
+ case "tool":
797
+ return { type: "function", function: { name: toolChoice.toolName } };
798
+ default:
799
+ return void 0;
800
+ }
801
+ }
802
+ convertPrompt(prompt) {
803
+ const messages = [];
804
+ for (const message of prompt) {
805
+ if (message.role === "system") {
806
+ messages.push({ role: "system", content: message.content });
807
+ continue;
808
+ }
809
+ if (message.role === "user") {
810
+ const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text);
811
+ if (textParts.length > 0) {
812
+ messages.push({ role: "user", content: textParts.join("\n") });
813
+ }
814
+ } else if (message.role === "assistant") {
815
+ const textParts = [];
816
+ const toolCalls = [];
817
+ for (const part of message.content) {
818
+ if (part.type === "text") {
819
+ textParts.push(part.text);
820
+ } else if (part.type === "tool-call") {
821
+ toolCalls.push({
822
+ id: part.toolCallId,
823
+ type: "function",
824
+ function: {
825
+ name: part.toolName,
826
+ arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input)
827
+ }
828
+ });
829
+ }
830
+ }
831
+ const assistantMessage = {
832
+ role: "assistant",
833
+ content: textParts.length > 0 ? textParts.join("\n") : null
834
+ };
835
+ if (toolCalls.length > 0) {
836
+ assistantMessage.tool_calls = toolCalls;
837
+ }
838
+ messages.push(assistantMessage);
839
+ } else if (message.role === "tool") {
840
+ for (const part of message.content) {
841
+ if (part.type === "tool-result") {
842
+ let resultContent;
843
+ if (part.output.type === "text") {
844
+ resultContent = part.output.value;
845
+ } else if (part.output.type === "json") {
846
+ resultContent = JSON.stringify(part.output.value);
847
+ } else if (part.output.type === "error-text") {
848
+ resultContent = part.output.value;
849
+ } else if (part.output.type === "error-json") {
850
+ resultContent = JSON.stringify(part.output.value);
851
+ } else {
852
+ resultContent = JSON.stringify(part.output);
853
+ }
854
+ messages.push({
855
+ role: "tool",
856
+ tool_call_id: part.toolCallId,
857
+ content: resultContent
858
+ });
859
+ }
860
+ }
861
+ }
862
+ }
863
+ return messages;
864
+ }
865
+ convertFinishReason(finishReason) {
866
+ switch (finishReason) {
867
+ case "stop":
868
+ return "stop";
869
+ case "length":
870
+ return "length";
871
+ case "tool_calls":
872
+ return "tool-calls";
873
+ case "content_filter":
874
+ return "content-filter";
875
+ default:
876
+ return "unknown";
877
+ }
878
+ }
879
+ /**
880
+ * Convert tools to Responses API format
881
+ */
882
+ convertToolsForResponses(tools) {
883
+ if (!tools || tools.length === 0) {
884
+ return void 0;
885
+ }
886
+ return tools.filter((tool) => tool.type === "function").map((tool) => {
887
+ const schema = { ...tool.inputSchema };
888
+ delete schema["$schema"];
889
+ return {
890
+ type: "function",
891
+ name: tool.name,
892
+ description: tool.description || "",
893
+ parameters: schema,
894
+ strict: false
895
+ };
896
+ });
897
+ }
898
+ /**
899
+ * Convert prompt to Responses API input format
900
+ */
901
+ convertPromptForResponses(prompt) {
902
+ const items = [];
903
+ for (const message of prompt) {
904
+ if (message.role === "system") {
905
+ continue;
906
+ }
907
+ if (message.role === "user") {
908
+ const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text);
909
+ if (textParts.length > 0) {
910
+ items.push({
911
+ type: "message",
912
+ role: "user",
913
+ content: textParts.map((text) => ({ type: "input_text", text }))
914
+ });
915
+ }
916
+ } else if (message.role === "assistant") {
917
+ const textParts = [];
918
+ for (const part of message.content) {
919
+ if (part.type === "text") {
920
+ textParts.push(part.text);
921
+ } else if (part.type === "tool-call") {
922
+ items.push({
923
+ type: "function_call",
924
+ call_id: part.toolCallId,
925
+ name: part.toolName,
926
+ arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input)
927
+ });
928
+ }
929
+ }
930
+ if (textParts.length > 0) {
931
+ items.push({
932
+ type: "message",
933
+ role: "assistant",
934
+ content: [{ type: "output_text", text: textParts.join("\n"), annotations: [] }]
935
+ });
936
+ }
937
+ } else if (message.role === "tool") {
938
+ for (const part of message.content) {
939
+ if (part.type === "tool-result") {
940
+ let resultContent;
941
+ if (part.output.type === "text") {
942
+ resultContent = part.output.value;
943
+ } else if (part.output.type === "json") {
944
+ resultContent = JSON.stringify(part.output.value);
945
+ } else if (part.output.type === "error-text") {
946
+ resultContent = part.output.value;
947
+ } else if (part.output.type === "error-json") {
948
+ resultContent = JSON.stringify(part.output.value);
949
+ } else {
950
+ resultContent = JSON.stringify(part.output);
951
+ }
952
+ items.push({
953
+ type: "function_call_output",
954
+ call_id: part.toolCallId,
955
+ output: resultContent
956
+ });
957
+ }
958
+ }
959
+ }
960
+ }
961
+ return items;
962
+ }
963
+ /**
964
+ * Extract system instructions from prompt
965
+ */
966
+ extractSystemInstructions(prompt) {
967
+ const systemMessages = prompt.filter((m) => m.role === "system").map((m) => m.content).join("\n");
968
+ return systemMessages || void 0;
969
+ }
970
+ /**
971
+ * Convert Responses API status to finish reason
972
+ * Note: Responses API returns 'completed' even when making tool calls,
973
+ * so we need to check the content for tool calls separately.
974
+ */
975
+ convertResponsesStatus(status, hasToolCalls = false) {
976
+ if (hasToolCalls) {
977
+ return "tool-calls";
978
+ }
979
+ switch (status) {
980
+ case "completed":
981
+ return "stop";
982
+ case "incomplete":
983
+ return "length";
984
+ case "cancelled":
985
+ return "stop";
986
+ case "failed":
987
+ return "error";
988
+ default:
989
+ return "unknown";
990
+ }
991
+ }
992
+ async doGenerate(options) {
993
+ if (this.useResponsesApi) {
994
+ return this.doGenerateWithResponsesApi(options, false);
995
+ }
996
+ return this.doGenerateWithChatApi(options, false);
997
+ }
998
+ async doGenerateWithChatApi(options, isRetry) {
999
+ const client = await this.getOpenAIClient(isRetry);
1000
+ const messages = this.convertPrompt(options.prompt);
1001
+ const tools = this.convertTools(options.tools);
1002
+ const toolChoice = options.toolChoice?.type !== "none" ? this.convertToolChoice(options.toolChoice) : void 0;
1003
+ const openaiModel = this.config.openaiModel || "gpt-4o";
1004
+ const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
1005
+ try {
1006
+ const response = await client.chat.completions.create({
1007
+ model: openaiModel,
1008
+ max_completion_tokens: maxTokens,
1009
+ messages,
1010
+ tools,
1011
+ tool_choice: tools ? toolChoice : void 0,
1012
+ temperature: options.temperature,
1013
+ top_p: options.topP,
1014
+ stop: options.stopSequences
1015
+ });
1016
+ const choice = response.choices[0];
1017
+ const content = [];
1018
+ if (choice?.message.content) {
1019
+ content.push({ type: "text", text: choice.message.content });
1020
+ }
1021
+ if (choice?.message.tool_calls) {
1022
+ for (const toolCall of choice.message.tool_calls) {
1023
+ if (toolCall.type === "function") {
1024
+ content.push({
1025
+ type: "tool-call",
1026
+ toolCallId: toolCall.id,
1027
+ toolName: toolCall.function.name,
1028
+ input: toolCall.function.arguments
1029
+ });
1030
+ }
1031
+ }
1032
+ }
1033
+ const usage = {
1034
+ inputTokens: response.usage?.prompt_tokens || 0,
1035
+ outputTokens: response.usage?.completion_tokens || 0,
1036
+ totalTokens: response.usage?.total_tokens || 0
1037
+ };
1038
+ return {
1039
+ content,
1040
+ finishReason: this.convertFinishReason(choice?.finish_reason),
1041
+ usage,
1042
+ warnings: []
1043
+ };
1044
+ } catch (error) {
1045
+ if (!isRetry && this.isTokenError(error)) {
1046
+ this.directAccessClient.invalidateToken();
1047
+ return this.doGenerateWithChatApi(options, true);
1048
+ }
1049
+ if (error instanceof import_openai.default.APIError) {
1050
+ throw new GitLabError({
1051
+ message: `OpenAI API error: ${error.message}`,
1052
+ cause: error
1053
+ });
1054
+ }
1055
+ throw error;
1056
+ }
1057
+ }
1058
+ async doGenerateWithResponsesApi(options, isRetry) {
1059
+ const client = await this.getOpenAIClient(isRetry);
1060
+ const input = this.convertPromptForResponses(options.prompt);
1061
+ const tools = this.convertToolsForResponses(options.tools);
1062
+ const instructions = this.extractSystemInstructions(options.prompt);
1063
+ const openaiModel = this.config.openaiModel || "gpt-5-codex";
1064
+ const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
1065
+ try {
1066
+ const response = await client.responses.create({
1067
+ model: openaiModel,
1068
+ input,
1069
+ instructions,
1070
+ tools,
1071
+ max_output_tokens: maxTokens,
1072
+ temperature: options.temperature,
1073
+ top_p: options.topP,
1074
+ store: false
1075
+ });
1076
+ const content = [];
1077
+ let hasToolCalls = false;
1078
+ for (const item of response.output || []) {
1079
+ if (item.type === "message" && item.role === "assistant") {
1080
+ for (const contentItem of item.content || []) {
1081
+ if (contentItem.type === "output_text") {
1082
+ content.push({ type: "text", text: contentItem.text });
1083
+ }
1084
+ }
1085
+ } else if (item.type === "function_call") {
1086
+ hasToolCalls = true;
1087
+ content.push({
1088
+ type: "tool-call",
1089
+ toolCallId: item.call_id,
1090
+ toolName: item.name,
1091
+ input: item.arguments
1092
+ });
1093
+ }
1094
+ }
1095
+ const usage = {
1096
+ inputTokens: response.usage?.input_tokens || 0,
1097
+ outputTokens: response.usage?.output_tokens || 0,
1098
+ totalTokens: response.usage?.total_tokens || 0
1099
+ };
1100
+ return {
1101
+ content,
1102
+ finishReason: this.convertResponsesStatus(response.status, hasToolCalls),
1103
+ usage,
1104
+ warnings: []
1105
+ };
1106
+ } catch (error) {
1107
+ if (!isRetry && this.isTokenError(error)) {
1108
+ this.directAccessClient.invalidateToken();
1109
+ return this.doGenerateWithResponsesApi(options, true);
1110
+ }
1111
+ if (error instanceof import_openai.default.APIError) {
1112
+ throw new GitLabError({
1113
+ message: `OpenAI API error: ${error.message}`,
1114
+ cause: error
1115
+ });
1116
+ }
1117
+ throw error;
1118
+ }
1119
+ }
1120
+ async doStream(options) {
1121
+ if (this.useResponsesApi) {
1122
+ return this.doStreamWithResponsesApi(options, false);
1123
+ }
1124
+ return this.doStreamWithChatApi(options, false);
1125
+ }
1126
+ async doStreamWithChatApi(options, isRetry) {
1127
+ const client = await this.getOpenAIClient(isRetry);
1128
+ const messages = this.convertPrompt(options.prompt);
1129
+ const tools = this.convertTools(options.tools);
1130
+ const toolChoice = options.toolChoice?.type !== "none" ? this.convertToolChoice(options.toolChoice) : void 0;
1131
+ const openaiModel = this.config.openaiModel || "gpt-4o";
1132
+ const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
1133
+ const requestBody = {
1134
+ model: openaiModel,
1135
+ max_completion_tokens: maxTokens,
1136
+ messages,
1137
+ tools,
1138
+ tool_choice: tools ? toolChoice : void 0,
1139
+ temperature: options.temperature,
1140
+ top_p: options.topP,
1141
+ stop: options.stopSequences,
1142
+ stream: true,
1143
+ stream_options: { include_usage: true }
1144
+ };
1145
+ const self = this;
1146
+ const stream = new ReadableStream({
1147
+ start: async (controller) => {
1148
+ const toolCalls = {};
1149
+ const usage = {
1150
+ inputTokens: 0,
1151
+ outputTokens: 0,
1152
+ totalTokens: 0
1153
+ };
1154
+ let finishReason = "unknown";
1155
+ let textStarted = false;
1156
+ const textId = "text-0";
1157
+ try {
1158
+ const openaiStream = await client.chat.completions.create({
1159
+ ...requestBody,
1160
+ stream: true
1161
+ });
1162
+ controller.enqueue({ type: "stream-start", warnings: [] });
1163
+ for await (const chunk of openaiStream) {
1164
+ const choice = chunk.choices?.[0];
1165
+ if (chunk.id && !textStarted) {
1166
+ controller.enqueue({
1167
+ type: "response-metadata",
1168
+ id: chunk.id,
1169
+ modelId: chunk.model
1170
+ });
1171
+ }
1172
+ if (choice?.delta?.content) {
1173
+ if (!textStarted) {
1174
+ controller.enqueue({ type: "text-start", id: textId });
1175
+ textStarted = true;
1176
+ }
1177
+ controller.enqueue({
1178
+ type: "text-delta",
1179
+ id: textId,
1180
+ delta: choice.delta.content
1181
+ });
1182
+ }
1183
+ if (choice?.delta?.tool_calls) {
1184
+ for (const tc of choice.delta.tool_calls) {
1185
+ const idx = tc.index;
1186
+ if (!toolCalls[idx]) {
1187
+ toolCalls[idx] = {
1188
+ id: tc.id || "",
1189
+ name: tc.function?.name || "",
1190
+ arguments: ""
1191
+ };
1192
+ controller.enqueue({
1193
+ type: "tool-input-start",
1194
+ id: toolCalls[idx].id,
1195
+ toolName: toolCalls[idx].name
1196
+ });
1197
+ }
1198
+ if (tc.function?.arguments) {
1199
+ toolCalls[idx].arguments += tc.function.arguments;
1200
+ controller.enqueue({
1201
+ type: "tool-input-delta",
1202
+ id: toolCalls[idx].id,
1203
+ delta: tc.function.arguments
1204
+ });
1205
+ }
1206
+ }
1207
+ }
1208
+ if (choice?.finish_reason) {
1209
+ finishReason = self.convertFinishReason(choice.finish_reason);
1210
+ }
1211
+ if (chunk.usage) {
1212
+ usage.inputTokens = chunk.usage.prompt_tokens || 0;
1213
+ usage.outputTokens = chunk.usage.completion_tokens || 0;
1214
+ usage.totalTokens = chunk.usage.total_tokens || 0;
1215
+ }
1216
+ }
1217
+ if (textStarted) {
1218
+ controller.enqueue({ type: "text-end", id: textId });
1219
+ }
1220
+ for (const [, tc] of Object.entries(toolCalls)) {
1221
+ controller.enqueue({ type: "tool-input-end", id: tc.id });
1222
+ controller.enqueue({
1223
+ type: "tool-call",
1224
+ toolCallId: tc.id,
1225
+ toolName: tc.name,
1226
+ input: tc.arguments || "{}"
1227
+ });
1228
+ }
1229
+ controller.enqueue({ type: "finish", finishReason, usage });
1230
+ controller.close();
1231
+ } catch (error) {
1232
+ if (!isRetry && self.isTokenError(error)) {
1233
+ self.directAccessClient.invalidateToken();
1234
+ controller.enqueue({
1235
+ type: "error",
1236
+ error: new GitLabError({ message: "TOKEN_REFRESH_NEEDED", cause: error })
1237
+ });
1238
+ controller.close();
1239
+ return;
1240
+ }
1241
+ if (error instanceof import_openai.default.APIError) {
1242
+ controller.enqueue({
1243
+ type: "error",
1244
+ error: new GitLabError({
1245
+ message: `OpenAI API error: ${error.message}`,
1246
+ cause: error
1247
+ })
1248
+ });
1249
+ } else {
1250
+ controller.enqueue({ type: "error", error });
1251
+ }
1252
+ controller.close();
1253
+ }
1254
+ }
1255
+ });
1256
+ return { stream, request: { body: requestBody } };
1257
+ }
1258
+ async doStreamWithResponsesApi(options, isRetry) {
1259
+ const client = await this.getOpenAIClient(isRetry);
1260
+ const input = this.convertPromptForResponses(options.prompt);
1261
+ const tools = this.convertToolsForResponses(options.tools);
1262
+ const instructions = this.extractSystemInstructions(options.prompt);
1263
+ const openaiModel = this.config.openaiModel || "gpt-5-codex";
1264
+ const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
1265
+ const requestBody = {
1266
+ model: openaiModel,
1267
+ input,
1268
+ instructions,
1269
+ tools,
1270
+ max_output_tokens: maxTokens,
1271
+ temperature: options.temperature,
1272
+ top_p: options.topP,
1273
+ store: false,
1274
+ stream: true
1275
+ };
1276
+ const self = this;
1277
+ const stream = new ReadableStream({
1278
+ start: async (controller) => {
1279
+ const toolCalls = {};
1280
+ const usage = {
1281
+ inputTokens: 0,
1282
+ outputTokens: 0,
1283
+ totalTokens: 0
1284
+ };
1285
+ let finishReason = "unknown";
1286
+ let textStarted = false;
1287
+ const textId = "text-0";
1288
+ try {
1289
+ const openaiStream = await client.responses.create({
1290
+ ...requestBody,
1291
+ stream: true
1292
+ });
1293
+ controller.enqueue({ type: "stream-start", warnings: [] });
1294
+ for await (const event of openaiStream) {
1295
+ if (event.type === "response.created") {
1296
+ controller.enqueue({
1297
+ type: "response-metadata",
1298
+ id: event.response.id,
1299
+ modelId: event.response.model
1300
+ });
1301
+ } else if (event.type === "response.output_item.added") {
1302
+ if (event.item.type === "function_call") {
1303
+ const outputIndex = event.output_index;
1304
+ const callId = event.item.call_id;
1305
+ toolCalls[outputIndex] = {
1306
+ callId,
1307
+ name: event.item.name,
1308
+ arguments: ""
1309
+ };
1310
+ controller.enqueue({
1311
+ type: "tool-input-start",
1312
+ id: callId,
1313
+ toolName: event.item.name
1314
+ });
1315
+ }
1316
+ } else if (event.type === "response.output_text.delta") {
1317
+ if (!textStarted) {
1318
+ controller.enqueue({ type: "text-start", id: textId });
1319
+ textStarted = true;
1320
+ }
1321
+ controller.enqueue({
1322
+ type: "text-delta",
1323
+ id: textId,
1324
+ delta: event.delta
1325
+ });
1326
+ } else if (event.type === "response.function_call_arguments.delta") {
1327
+ const outputIndex = event.output_index;
1328
+ const tc = toolCalls[outputIndex];
1329
+ if (tc) {
1330
+ tc.arguments += event.delta;
1331
+ controller.enqueue({
1332
+ type: "tool-input-delta",
1333
+ id: tc.callId,
1334
+ delta: event.delta
1335
+ });
1336
+ }
1337
+ } else if (event.type === "response.function_call_arguments.done") {
1338
+ const outputIndex = event.output_index;
1339
+ const tc = toolCalls[outputIndex];
1340
+ if (tc) {
1341
+ tc.arguments = event.arguments;
1342
+ }
1343
+ } else if (event.type === "response.completed") {
1344
+ const hasToolCalls2 = Object.keys(toolCalls).length > 0;
1345
+ finishReason = self.convertResponsesStatus(event.response.status, hasToolCalls2);
1346
+ if (event.response.usage) {
1347
+ usage.inputTokens = event.response.usage.input_tokens || 0;
1348
+ usage.outputTokens = event.response.usage.output_tokens || 0;
1349
+ usage.totalTokens = event.response.usage.total_tokens || 0;
1350
+ }
1351
+ }
1352
+ }
1353
+ if (textStarted) {
1354
+ controller.enqueue({ type: "text-end", id: textId });
1355
+ }
1356
+ const hasToolCalls = Object.keys(toolCalls).length > 0;
1357
+ if (hasToolCalls && finishReason === "stop") {
1358
+ finishReason = "tool-calls";
1359
+ }
1360
+ for (const tc of Object.values(toolCalls)) {
1361
+ controller.enqueue({ type: "tool-input-end", id: tc.callId });
1362
+ controller.enqueue({
1363
+ type: "tool-call",
1364
+ toolCallId: tc.callId,
1365
+ toolName: tc.name,
1366
+ input: tc.arguments || "{}"
1367
+ });
1368
+ }
1369
+ controller.enqueue({ type: "finish", finishReason, usage });
1370
+ controller.close();
1371
+ } catch (error) {
1372
+ if (!isRetry && self.isTokenError(error)) {
1373
+ self.directAccessClient.invalidateToken();
1374
+ controller.enqueue({
1375
+ type: "error",
1376
+ error: new GitLabError({ message: "TOKEN_REFRESH_NEEDED", cause: error })
1377
+ });
1378
+ controller.close();
1379
+ return;
1380
+ }
1381
+ if (error instanceof import_openai.default.APIError) {
1382
+ controller.enqueue({
1383
+ type: "error",
1384
+ error: new GitLabError({
1385
+ message: `OpenAI API error: ${error.message}`,
1386
+ cause: error
1387
+ })
1388
+ });
1389
+ } else {
1390
+ controller.enqueue({ type: "error", error });
1391
+ }
1392
+ controller.close();
1393
+ }
1394
+ }
1395
+ });
1396
+ return { stream, request: { body: requestBody } };
1397
+ }
1398
+ };
1399
+
648
1400
  // src/gitlab-oauth-types.ts
649
1401
  var BUNDLED_CLIENT_ID = "36f2a70cddeb5a0889d4fd8295c241b7e9848e89cf9e599d0eed2d8e5350fbf5";
650
1402
  var GITLAB_COM_URL = "https://gitlab.com";
@@ -807,16 +1559,6 @@ var GitLabOAuthManager = class {
807
1559
  }
808
1560
  };
809
1561
 
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
1562
  // src/gitlab-provider.ts
821
1563
  var fs = __toESM(require("fs"));
822
1564
  var path = __toESM(require("path"));
@@ -952,21 +1694,44 @@ function createGitLab(options = {}) {
952
1694
  getApiKey().catch(() => {
953
1695
  });
954
1696
  const createAgenticChatModel = (modelId, agenticOptions) => {
1697
+ const mapping = getModelMapping(modelId);
1698
+ if (!mapping) {
1699
+ throw new GitLabError({
1700
+ message: `Unknown model ID: ${modelId}. Model must be registered in MODEL_MAPPINGS.`
1701
+ });
1702
+ }
1703
+ if (agenticOptions?.providerModel) {
1704
+ const validModels = getValidModelsForProvider(mapping.provider);
1705
+ if (!validModels.includes(agenticOptions.providerModel)) {
1706
+ throw new GitLabError({
1707
+ message: `Invalid providerModel '${agenticOptions.providerModel}' for provider '${mapping.provider}'. Valid models: ${validModels.join(", ")}`
1708
+ });
1709
+ }
1710
+ }
955
1711
  const featureFlags = {
956
1712
  DuoAgentPlatformNext: true,
957
1713
  ...options.featureFlags,
958
1714
  ...agenticOptions?.featureFlags
959
1715
  };
960
- return new GitLabAgenticLanguageModel(modelId, {
1716
+ const baseConfig = {
961
1717
  provider: `${providerName}.agentic`,
962
1718
  instanceUrl,
963
1719
  getHeaders,
964
1720
  refreshApiKey,
965
1721
  fetch: options.fetch,
966
- anthropicModel: agenticOptions?.anthropicModel ?? getAnthropicModelForModelId(modelId),
967
1722
  maxTokens: agenticOptions?.maxTokens,
968
1723
  featureFlags,
969
1724
  aiGatewayUrl: options.aiGatewayUrl
1725
+ };
1726
+ if (mapping.provider === "openai") {
1727
+ return new GitLabOpenAILanguageModel(modelId, {
1728
+ ...baseConfig,
1729
+ openaiModel: agenticOptions?.providerModel ?? mapping.model
1730
+ });
1731
+ }
1732
+ return new GitLabAnthropicLanguageModel(modelId, {
1733
+ ...baseConfig,
1734
+ anthropicModel: agenticOptions?.providerModel ?? mapping.model
970
1735
  });
971
1736
  };
972
1737
  const createDefaultModel = (modelId) => {
@@ -1266,17 +2031,25 @@ var GitLabProjectDetector = class {
1266
2031
  BUNDLED_CLIENT_ID,
1267
2032
  DEFAULT_AI_GATEWAY_URL,
1268
2033
  GITLAB_COM_URL,
1269
- GitLabAgenticLanguageModel,
2034
+ GitLabAnthropicLanguageModel,
1270
2035
  GitLabDirectAccessClient,
1271
2036
  GitLabError,
1272
2037
  GitLabOAuthManager,
2038
+ GitLabOpenAILanguageModel,
1273
2039
  GitLabProjectCache,
1274
2040
  GitLabProjectDetector,
1275
2041
  MODEL_ID_TO_ANTHROPIC_MODEL,
2042
+ MODEL_MAPPINGS,
1276
2043
  OAUTH_SCOPES,
1277
2044
  TOKEN_EXPIRY_SKEW_MS,
1278
2045
  createGitLab,
1279
2046
  getAnthropicModelForModelId,
1280
- gitlab
2047
+ getModelMapping,
2048
+ getOpenAIApiType,
2049
+ getOpenAIModelForModelId,
2050
+ getProviderForModelId,
2051
+ getValidModelsForProvider,
2052
+ gitlab,
2053
+ isResponsesApiModel
1281
2054
  });
1282
2055
  //# sourceMappingURL=index.js.map