@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/CHANGELOG.md +8 -0
- package/README.md +117 -29
- package/dist/gitlab-gitlab-ai-provider-3.3.0.tgz +0 -0
- package/dist/index.d.mts +113 -46
- package/dist/index.d.ts +113 -46
- package/dist/index.js +792 -19
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +782 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/dist/gitlab-gitlab-ai-provider-3.1.3.tgz +0 -0
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// src/gitlab-
|
|
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,8 +151,8 @@ var GitLabDirectAccessClient = class {
|
|
|
142
151
|
}
|
|
143
152
|
};
|
|
144
153
|
|
|
145
|
-
// src/gitlab-
|
|
146
|
-
var
|
|
154
|
+
// src/gitlab-anthropic-language-model.ts
|
|
155
|
+
var GitLabAnthropicLanguageModel = class {
|
|
147
156
|
specificationVersion = "v2";
|
|
148
157
|
modelId;
|
|
149
158
|
supportedUrls = {};
|
|
@@ -596,6 +605,741 @@ var GitLabAgenticLanguageModel = class {
|
|
|
596
605
|
}
|
|
597
606
|
};
|
|
598
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-2": { provider: "openai", model: "gpt-5.2-2025-12-11", openaiApiType: "chat" },
|
|
620
|
+
"duo-chat-gpt-5-mini": {
|
|
621
|
+
provider: "openai",
|
|
622
|
+
model: "gpt-5-mini-2025-08-07",
|
|
623
|
+
openaiApiType: "chat"
|
|
624
|
+
},
|
|
625
|
+
// OpenAI models - Responses API (Codex models)
|
|
626
|
+
"duo-chat-gpt-5-codex": { provider: "openai", model: "gpt-5-codex", openaiApiType: "responses" },
|
|
627
|
+
"duo-chat-gpt-5-2-codex": {
|
|
628
|
+
provider: "openai",
|
|
629
|
+
model: "gpt-5.2-codex",
|
|
630
|
+
openaiApiType: "responses"
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
function getModelMapping(modelId) {
|
|
634
|
+
return MODEL_MAPPINGS[modelId];
|
|
635
|
+
}
|
|
636
|
+
function getProviderForModelId(modelId) {
|
|
637
|
+
return MODEL_MAPPINGS[modelId]?.provider;
|
|
638
|
+
}
|
|
639
|
+
function getValidModelsForProvider(provider) {
|
|
640
|
+
return Object.values(MODEL_MAPPINGS).filter((m) => m.provider === provider).map((m) => m.model);
|
|
641
|
+
}
|
|
642
|
+
function getAnthropicModelForModelId(modelId) {
|
|
643
|
+
const mapping = MODEL_MAPPINGS[modelId];
|
|
644
|
+
return mapping?.provider === "anthropic" ? mapping.model : void 0;
|
|
645
|
+
}
|
|
646
|
+
function getOpenAIModelForModelId(modelId) {
|
|
647
|
+
const mapping = MODEL_MAPPINGS[modelId];
|
|
648
|
+
return mapping?.provider === "openai" ? mapping.model : void 0;
|
|
649
|
+
}
|
|
650
|
+
function getOpenAIApiType(modelId) {
|
|
651
|
+
const mapping = MODEL_MAPPINGS[modelId];
|
|
652
|
+
return mapping?.openaiApiType ?? "chat";
|
|
653
|
+
}
|
|
654
|
+
function isResponsesApiModel(modelId) {
|
|
655
|
+
return getOpenAIApiType(modelId) === "responses";
|
|
656
|
+
}
|
|
657
|
+
var MODEL_ID_TO_ANTHROPIC_MODEL = Object.fromEntries(
|
|
658
|
+
Object.entries(MODEL_MAPPINGS).filter(([, v]) => v.provider === "anthropic").map(([k, v]) => [k, v.model])
|
|
659
|
+
);
|
|
660
|
+
|
|
661
|
+
// src/gitlab-openai-language-model.ts
|
|
662
|
+
var GitLabOpenAILanguageModel = class {
|
|
663
|
+
specificationVersion = "v2";
|
|
664
|
+
modelId;
|
|
665
|
+
supportedUrls = {};
|
|
666
|
+
config;
|
|
667
|
+
directAccessClient;
|
|
668
|
+
useResponsesApi;
|
|
669
|
+
openaiClient = null;
|
|
670
|
+
constructor(modelId, config) {
|
|
671
|
+
this.modelId = modelId;
|
|
672
|
+
this.config = config;
|
|
673
|
+
this.useResponsesApi = config.useResponsesApi ?? isResponsesApiModel(modelId);
|
|
674
|
+
this.directAccessClient = new GitLabDirectAccessClient({
|
|
675
|
+
instanceUrl: config.instanceUrl,
|
|
676
|
+
getHeaders: config.getHeaders,
|
|
677
|
+
refreshApiKey: config.refreshApiKey,
|
|
678
|
+
fetch: config.fetch,
|
|
679
|
+
featureFlags: config.featureFlags,
|
|
680
|
+
aiGatewayUrl: config.aiGatewayUrl
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
get provider() {
|
|
684
|
+
return this.config.provider;
|
|
685
|
+
}
|
|
686
|
+
async getOpenAIClient(forceRefresh = false) {
|
|
687
|
+
const tokenData = await this.directAccessClient.getDirectAccessToken(forceRefresh);
|
|
688
|
+
const { "x-api-key": _removed, ...filteredHeaders } = tokenData.headers;
|
|
689
|
+
this.openaiClient = new OpenAI({
|
|
690
|
+
apiKey: tokenData.token,
|
|
691
|
+
baseURL: this.directAccessClient.getOpenAIProxyUrl(),
|
|
692
|
+
defaultHeaders: filteredHeaders
|
|
693
|
+
});
|
|
694
|
+
return this.openaiClient;
|
|
695
|
+
}
|
|
696
|
+
isTokenError(error) {
|
|
697
|
+
if (error instanceof OpenAI.APIError) {
|
|
698
|
+
if (error.status === 401) {
|
|
699
|
+
return true;
|
|
700
|
+
}
|
|
701
|
+
const message = error.message?.toLowerCase() || "";
|
|
702
|
+
if (message.includes("token") && (message.includes("expired") || message.includes("revoked") || message.includes("invalid"))) {
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
707
|
+
}
|
|
708
|
+
convertTools(tools) {
|
|
709
|
+
if (!tools || tools.length === 0) {
|
|
710
|
+
return void 0;
|
|
711
|
+
}
|
|
712
|
+
return tools.filter((tool) => tool.type === "function").map((tool) => {
|
|
713
|
+
const schema = tool.inputSchema;
|
|
714
|
+
return {
|
|
715
|
+
type: "function",
|
|
716
|
+
function: {
|
|
717
|
+
name: tool.name,
|
|
718
|
+
description: tool.description || "",
|
|
719
|
+
// Ensure the schema has type: 'object' as OpenAI requires it
|
|
720
|
+
parameters: {
|
|
721
|
+
type: "object",
|
|
722
|
+
...schema
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
convertToolChoice(toolChoice) {
|
|
729
|
+
if (!toolChoice) {
|
|
730
|
+
return void 0;
|
|
731
|
+
}
|
|
732
|
+
switch (toolChoice.type) {
|
|
733
|
+
case "auto":
|
|
734
|
+
return "auto";
|
|
735
|
+
case "none":
|
|
736
|
+
return "none";
|
|
737
|
+
case "required":
|
|
738
|
+
return "required";
|
|
739
|
+
case "tool":
|
|
740
|
+
return { type: "function", function: { name: toolChoice.toolName } };
|
|
741
|
+
default:
|
|
742
|
+
return void 0;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
convertPrompt(prompt) {
|
|
746
|
+
const messages = [];
|
|
747
|
+
for (const message of prompt) {
|
|
748
|
+
if (message.role === "system") {
|
|
749
|
+
messages.push({ role: "system", content: message.content });
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (message.role === "user") {
|
|
753
|
+
const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text);
|
|
754
|
+
if (textParts.length > 0) {
|
|
755
|
+
messages.push({ role: "user", content: textParts.join("\n") });
|
|
756
|
+
}
|
|
757
|
+
} else if (message.role === "assistant") {
|
|
758
|
+
const textParts = [];
|
|
759
|
+
const toolCalls = [];
|
|
760
|
+
for (const part of message.content) {
|
|
761
|
+
if (part.type === "text") {
|
|
762
|
+
textParts.push(part.text);
|
|
763
|
+
} else if (part.type === "tool-call") {
|
|
764
|
+
toolCalls.push({
|
|
765
|
+
id: part.toolCallId,
|
|
766
|
+
type: "function",
|
|
767
|
+
function: {
|
|
768
|
+
name: part.toolName,
|
|
769
|
+
arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input)
|
|
770
|
+
}
|
|
771
|
+
});
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const assistantMessage = {
|
|
775
|
+
role: "assistant",
|
|
776
|
+
content: textParts.length > 0 ? textParts.join("\n") : null
|
|
777
|
+
};
|
|
778
|
+
if (toolCalls.length > 0) {
|
|
779
|
+
assistantMessage.tool_calls = toolCalls;
|
|
780
|
+
}
|
|
781
|
+
messages.push(assistantMessage);
|
|
782
|
+
} else if (message.role === "tool") {
|
|
783
|
+
for (const part of message.content) {
|
|
784
|
+
if (part.type === "tool-result") {
|
|
785
|
+
let resultContent;
|
|
786
|
+
if (part.output.type === "text") {
|
|
787
|
+
resultContent = part.output.value;
|
|
788
|
+
} else if (part.output.type === "json") {
|
|
789
|
+
resultContent = JSON.stringify(part.output.value);
|
|
790
|
+
} else if (part.output.type === "error-text") {
|
|
791
|
+
resultContent = part.output.value;
|
|
792
|
+
} else if (part.output.type === "error-json") {
|
|
793
|
+
resultContent = JSON.stringify(part.output.value);
|
|
794
|
+
} else {
|
|
795
|
+
resultContent = JSON.stringify(part.output);
|
|
796
|
+
}
|
|
797
|
+
messages.push({
|
|
798
|
+
role: "tool",
|
|
799
|
+
tool_call_id: part.toolCallId,
|
|
800
|
+
content: resultContent
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return messages;
|
|
807
|
+
}
|
|
808
|
+
convertFinishReason(finishReason) {
|
|
809
|
+
switch (finishReason) {
|
|
810
|
+
case "stop":
|
|
811
|
+
return "stop";
|
|
812
|
+
case "length":
|
|
813
|
+
return "length";
|
|
814
|
+
case "tool_calls":
|
|
815
|
+
return "tool-calls";
|
|
816
|
+
case "content_filter":
|
|
817
|
+
return "content-filter";
|
|
818
|
+
default:
|
|
819
|
+
return "unknown";
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
/**
|
|
823
|
+
* Convert tools to Responses API format
|
|
824
|
+
*/
|
|
825
|
+
convertToolsForResponses(tools) {
|
|
826
|
+
if (!tools || tools.length === 0) {
|
|
827
|
+
return void 0;
|
|
828
|
+
}
|
|
829
|
+
return tools.filter((tool) => tool.type === "function").map((tool) => {
|
|
830
|
+
const schema = { ...tool.inputSchema };
|
|
831
|
+
delete schema["$schema"];
|
|
832
|
+
return {
|
|
833
|
+
type: "function",
|
|
834
|
+
name: tool.name,
|
|
835
|
+
description: tool.description || "",
|
|
836
|
+
parameters: schema,
|
|
837
|
+
strict: false
|
|
838
|
+
};
|
|
839
|
+
});
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* Convert prompt to Responses API input format
|
|
843
|
+
*/
|
|
844
|
+
convertPromptForResponses(prompt) {
|
|
845
|
+
const items = [];
|
|
846
|
+
for (const message of prompt) {
|
|
847
|
+
if (message.role === "system") {
|
|
848
|
+
continue;
|
|
849
|
+
}
|
|
850
|
+
if (message.role === "user") {
|
|
851
|
+
const textParts = message.content.filter((part) => part.type === "text").map((part) => part.text);
|
|
852
|
+
if (textParts.length > 0) {
|
|
853
|
+
items.push({
|
|
854
|
+
type: "message",
|
|
855
|
+
role: "user",
|
|
856
|
+
content: textParts.map((text) => ({ type: "input_text", text }))
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
} else if (message.role === "assistant") {
|
|
860
|
+
const textParts = [];
|
|
861
|
+
for (const part of message.content) {
|
|
862
|
+
if (part.type === "text") {
|
|
863
|
+
textParts.push(part.text);
|
|
864
|
+
} else if (part.type === "tool-call") {
|
|
865
|
+
items.push({
|
|
866
|
+
type: "function_call",
|
|
867
|
+
call_id: part.toolCallId,
|
|
868
|
+
name: part.toolName,
|
|
869
|
+
arguments: typeof part.input === "string" ? part.input : JSON.stringify(part.input)
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
if (textParts.length > 0) {
|
|
874
|
+
items.push({
|
|
875
|
+
type: "message",
|
|
876
|
+
role: "assistant",
|
|
877
|
+
content: [{ type: "output_text", text: textParts.join("\n"), annotations: [] }]
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
} else if (message.role === "tool") {
|
|
881
|
+
for (const part of message.content) {
|
|
882
|
+
if (part.type === "tool-result") {
|
|
883
|
+
let resultContent;
|
|
884
|
+
if (part.output.type === "text") {
|
|
885
|
+
resultContent = part.output.value;
|
|
886
|
+
} else if (part.output.type === "json") {
|
|
887
|
+
resultContent = JSON.stringify(part.output.value);
|
|
888
|
+
} else if (part.output.type === "error-text") {
|
|
889
|
+
resultContent = part.output.value;
|
|
890
|
+
} else if (part.output.type === "error-json") {
|
|
891
|
+
resultContent = JSON.stringify(part.output.value);
|
|
892
|
+
} else {
|
|
893
|
+
resultContent = JSON.stringify(part.output);
|
|
894
|
+
}
|
|
895
|
+
items.push({
|
|
896
|
+
type: "function_call_output",
|
|
897
|
+
call_id: part.toolCallId,
|
|
898
|
+
output: resultContent
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
return items;
|
|
905
|
+
}
|
|
906
|
+
/**
|
|
907
|
+
* Extract system instructions from prompt
|
|
908
|
+
*/
|
|
909
|
+
extractSystemInstructions(prompt) {
|
|
910
|
+
const systemMessages = prompt.filter((m) => m.role === "system").map((m) => m.content).join("\n");
|
|
911
|
+
return systemMessages || void 0;
|
|
912
|
+
}
|
|
913
|
+
/**
|
|
914
|
+
* Convert Responses API status to finish reason
|
|
915
|
+
* Note: Responses API returns 'completed' even when making tool calls,
|
|
916
|
+
* so we need to check the content for tool calls separately.
|
|
917
|
+
*/
|
|
918
|
+
convertResponsesStatus(status, hasToolCalls = false) {
|
|
919
|
+
if (hasToolCalls) {
|
|
920
|
+
return "tool-calls";
|
|
921
|
+
}
|
|
922
|
+
switch (status) {
|
|
923
|
+
case "completed":
|
|
924
|
+
return "stop";
|
|
925
|
+
case "incomplete":
|
|
926
|
+
return "length";
|
|
927
|
+
case "cancelled":
|
|
928
|
+
return "stop";
|
|
929
|
+
case "failed":
|
|
930
|
+
return "error";
|
|
931
|
+
default:
|
|
932
|
+
return "unknown";
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
async doGenerate(options) {
|
|
936
|
+
if (this.useResponsesApi) {
|
|
937
|
+
return this.doGenerateWithResponsesApi(options, false);
|
|
938
|
+
}
|
|
939
|
+
return this.doGenerateWithChatApi(options, false);
|
|
940
|
+
}
|
|
941
|
+
async doGenerateWithChatApi(options, isRetry) {
|
|
942
|
+
const client = await this.getOpenAIClient(isRetry);
|
|
943
|
+
const messages = this.convertPrompt(options.prompt);
|
|
944
|
+
const tools = this.convertTools(options.tools);
|
|
945
|
+
const toolChoice = options.toolChoice?.type !== "none" ? this.convertToolChoice(options.toolChoice) : void 0;
|
|
946
|
+
const openaiModel = this.config.openaiModel || "gpt-4o";
|
|
947
|
+
const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
|
|
948
|
+
try {
|
|
949
|
+
const response = await client.chat.completions.create({
|
|
950
|
+
model: openaiModel,
|
|
951
|
+
max_completion_tokens: maxTokens,
|
|
952
|
+
messages,
|
|
953
|
+
tools,
|
|
954
|
+
tool_choice: tools ? toolChoice : void 0,
|
|
955
|
+
temperature: options.temperature,
|
|
956
|
+
top_p: options.topP,
|
|
957
|
+
stop: options.stopSequences
|
|
958
|
+
});
|
|
959
|
+
const choice = response.choices[0];
|
|
960
|
+
const content = [];
|
|
961
|
+
if (choice?.message.content) {
|
|
962
|
+
content.push({ type: "text", text: choice.message.content });
|
|
963
|
+
}
|
|
964
|
+
if (choice?.message.tool_calls) {
|
|
965
|
+
for (const toolCall of choice.message.tool_calls) {
|
|
966
|
+
if (toolCall.type === "function") {
|
|
967
|
+
content.push({
|
|
968
|
+
type: "tool-call",
|
|
969
|
+
toolCallId: toolCall.id,
|
|
970
|
+
toolName: toolCall.function.name,
|
|
971
|
+
input: toolCall.function.arguments
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
const usage = {
|
|
977
|
+
inputTokens: response.usage?.prompt_tokens || 0,
|
|
978
|
+
outputTokens: response.usage?.completion_tokens || 0,
|
|
979
|
+
totalTokens: response.usage?.total_tokens || 0
|
|
980
|
+
};
|
|
981
|
+
return {
|
|
982
|
+
content,
|
|
983
|
+
finishReason: this.convertFinishReason(choice?.finish_reason),
|
|
984
|
+
usage,
|
|
985
|
+
warnings: []
|
|
986
|
+
};
|
|
987
|
+
} catch (error) {
|
|
988
|
+
if (!isRetry && this.isTokenError(error)) {
|
|
989
|
+
this.directAccessClient.invalidateToken();
|
|
990
|
+
return this.doGenerateWithChatApi(options, true);
|
|
991
|
+
}
|
|
992
|
+
if (error instanceof OpenAI.APIError) {
|
|
993
|
+
throw new GitLabError({
|
|
994
|
+
message: `OpenAI API error: ${error.message}`,
|
|
995
|
+
cause: error
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
throw error;
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
async doGenerateWithResponsesApi(options, isRetry) {
|
|
1002
|
+
const client = await this.getOpenAIClient(isRetry);
|
|
1003
|
+
const input = this.convertPromptForResponses(options.prompt);
|
|
1004
|
+
const tools = this.convertToolsForResponses(options.tools);
|
|
1005
|
+
const instructions = this.extractSystemInstructions(options.prompt);
|
|
1006
|
+
const openaiModel = this.config.openaiModel || "gpt-5-codex";
|
|
1007
|
+
const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
|
|
1008
|
+
try {
|
|
1009
|
+
const response = await client.responses.create({
|
|
1010
|
+
model: openaiModel,
|
|
1011
|
+
input,
|
|
1012
|
+
instructions,
|
|
1013
|
+
tools,
|
|
1014
|
+
max_output_tokens: maxTokens,
|
|
1015
|
+
temperature: options.temperature,
|
|
1016
|
+
top_p: options.topP,
|
|
1017
|
+
store: false
|
|
1018
|
+
});
|
|
1019
|
+
const content = [];
|
|
1020
|
+
let hasToolCalls = false;
|
|
1021
|
+
for (const item of response.output || []) {
|
|
1022
|
+
if (item.type === "message" && item.role === "assistant") {
|
|
1023
|
+
for (const contentItem of item.content || []) {
|
|
1024
|
+
if (contentItem.type === "output_text") {
|
|
1025
|
+
content.push({ type: "text", text: contentItem.text });
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
} else if (item.type === "function_call") {
|
|
1029
|
+
hasToolCalls = true;
|
|
1030
|
+
content.push({
|
|
1031
|
+
type: "tool-call",
|
|
1032
|
+
toolCallId: item.call_id,
|
|
1033
|
+
toolName: item.name,
|
|
1034
|
+
input: item.arguments
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
const usage = {
|
|
1039
|
+
inputTokens: response.usage?.input_tokens || 0,
|
|
1040
|
+
outputTokens: response.usage?.output_tokens || 0,
|
|
1041
|
+
totalTokens: response.usage?.total_tokens || 0
|
|
1042
|
+
};
|
|
1043
|
+
return {
|
|
1044
|
+
content,
|
|
1045
|
+
finishReason: this.convertResponsesStatus(response.status, hasToolCalls),
|
|
1046
|
+
usage,
|
|
1047
|
+
warnings: []
|
|
1048
|
+
};
|
|
1049
|
+
} catch (error) {
|
|
1050
|
+
if (!isRetry && this.isTokenError(error)) {
|
|
1051
|
+
this.directAccessClient.invalidateToken();
|
|
1052
|
+
return this.doGenerateWithResponsesApi(options, true);
|
|
1053
|
+
}
|
|
1054
|
+
if (error instanceof OpenAI.APIError) {
|
|
1055
|
+
throw new GitLabError({
|
|
1056
|
+
message: `OpenAI API error: ${error.message}`,
|
|
1057
|
+
cause: error
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
throw error;
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
async doStream(options) {
|
|
1064
|
+
if (this.useResponsesApi) {
|
|
1065
|
+
return this.doStreamWithResponsesApi(options, false);
|
|
1066
|
+
}
|
|
1067
|
+
return this.doStreamWithChatApi(options, false);
|
|
1068
|
+
}
|
|
1069
|
+
async doStreamWithChatApi(options, isRetry) {
|
|
1070
|
+
const client = await this.getOpenAIClient(isRetry);
|
|
1071
|
+
const messages = this.convertPrompt(options.prompt);
|
|
1072
|
+
const tools = this.convertTools(options.tools);
|
|
1073
|
+
const toolChoice = options.toolChoice?.type !== "none" ? this.convertToolChoice(options.toolChoice) : void 0;
|
|
1074
|
+
const openaiModel = this.config.openaiModel || "gpt-4o";
|
|
1075
|
+
const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
|
|
1076
|
+
const requestBody = {
|
|
1077
|
+
model: openaiModel,
|
|
1078
|
+
max_completion_tokens: maxTokens,
|
|
1079
|
+
messages,
|
|
1080
|
+
tools,
|
|
1081
|
+
tool_choice: tools ? toolChoice : void 0,
|
|
1082
|
+
temperature: options.temperature,
|
|
1083
|
+
top_p: options.topP,
|
|
1084
|
+
stop: options.stopSequences,
|
|
1085
|
+
stream: true,
|
|
1086
|
+
stream_options: { include_usage: true }
|
|
1087
|
+
};
|
|
1088
|
+
const self = this;
|
|
1089
|
+
const stream = new ReadableStream({
|
|
1090
|
+
start: async (controller) => {
|
|
1091
|
+
const toolCalls = {};
|
|
1092
|
+
const usage = {
|
|
1093
|
+
inputTokens: 0,
|
|
1094
|
+
outputTokens: 0,
|
|
1095
|
+
totalTokens: 0
|
|
1096
|
+
};
|
|
1097
|
+
let finishReason = "unknown";
|
|
1098
|
+
let textStarted = false;
|
|
1099
|
+
const textId = "text-0";
|
|
1100
|
+
try {
|
|
1101
|
+
const openaiStream = await client.chat.completions.create({
|
|
1102
|
+
...requestBody,
|
|
1103
|
+
stream: true
|
|
1104
|
+
});
|
|
1105
|
+
controller.enqueue({ type: "stream-start", warnings: [] });
|
|
1106
|
+
for await (const chunk of openaiStream) {
|
|
1107
|
+
const choice = chunk.choices?.[0];
|
|
1108
|
+
if (chunk.id && !textStarted) {
|
|
1109
|
+
controller.enqueue({
|
|
1110
|
+
type: "response-metadata",
|
|
1111
|
+
id: chunk.id,
|
|
1112
|
+
modelId: chunk.model
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
if (choice?.delta?.content) {
|
|
1116
|
+
if (!textStarted) {
|
|
1117
|
+
controller.enqueue({ type: "text-start", id: textId });
|
|
1118
|
+
textStarted = true;
|
|
1119
|
+
}
|
|
1120
|
+
controller.enqueue({
|
|
1121
|
+
type: "text-delta",
|
|
1122
|
+
id: textId,
|
|
1123
|
+
delta: choice.delta.content
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
1126
|
+
if (choice?.delta?.tool_calls) {
|
|
1127
|
+
for (const tc of choice.delta.tool_calls) {
|
|
1128
|
+
const idx = tc.index;
|
|
1129
|
+
if (!toolCalls[idx]) {
|
|
1130
|
+
toolCalls[idx] = {
|
|
1131
|
+
id: tc.id || "",
|
|
1132
|
+
name: tc.function?.name || "",
|
|
1133
|
+
arguments: ""
|
|
1134
|
+
};
|
|
1135
|
+
controller.enqueue({
|
|
1136
|
+
type: "tool-input-start",
|
|
1137
|
+
id: toolCalls[idx].id,
|
|
1138
|
+
toolName: toolCalls[idx].name
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
if (tc.function?.arguments) {
|
|
1142
|
+
toolCalls[idx].arguments += tc.function.arguments;
|
|
1143
|
+
controller.enqueue({
|
|
1144
|
+
type: "tool-input-delta",
|
|
1145
|
+
id: toolCalls[idx].id,
|
|
1146
|
+
delta: tc.function.arguments
|
|
1147
|
+
});
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
if (choice?.finish_reason) {
|
|
1152
|
+
finishReason = self.convertFinishReason(choice.finish_reason);
|
|
1153
|
+
}
|
|
1154
|
+
if (chunk.usage) {
|
|
1155
|
+
usage.inputTokens = chunk.usage.prompt_tokens || 0;
|
|
1156
|
+
usage.outputTokens = chunk.usage.completion_tokens || 0;
|
|
1157
|
+
usage.totalTokens = chunk.usage.total_tokens || 0;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
if (textStarted) {
|
|
1161
|
+
controller.enqueue({ type: "text-end", id: textId });
|
|
1162
|
+
}
|
|
1163
|
+
for (const [, tc] of Object.entries(toolCalls)) {
|
|
1164
|
+
controller.enqueue({ type: "tool-input-end", id: tc.id });
|
|
1165
|
+
controller.enqueue({
|
|
1166
|
+
type: "tool-call",
|
|
1167
|
+
toolCallId: tc.id,
|
|
1168
|
+
toolName: tc.name,
|
|
1169
|
+
input: tc.arguments || "{}"
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
controller.enqueue({ type: "finish", finishReason, usage });
|
|
1173
|
+
controller.close();
|
|
1174
|
+
} catch (error) {
|
|
1175
|
+
if (!isRetry && self.isTokenError(error)) {
|
|
1176
|
+
self.directAccessClient.invalidateToken();
|
|
1177
|
+
controller.enqueue({
|
|
1178
|
+
type: "error",
|
|
1179
|
+
error: new GitLabError({ message: "TOKEN_REFRESH_NEEDED", cause: error })
|
|
1180
|
+
});
|
|
1181
|
+
controller.close();
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
if (error instanceof OpenAI.APIError) {
|
|
1185
|
+
controller.enqueue({
|
|
1186
|
+
type: "error",
|
|
1187
|
+
error: new GitLabError({
|
|
1188
|
+
message: `OpenAI API error: ${error.message}`,
|
|
1189
|
+
cause: error
|
|
1190
|
+
})
|
|
1191
|
+
});
|
|
1192
|
+
} else {
|
|
1193
|
+
controller.enqueue({ type: "error", error });
|
|
1194
|
+
}
|
|
1195
|
+
controller.close();
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1198
|
+
});
|
|
1199
|
+
return { stream, request: { body: requestBody } };
|
|
1200
|
+
}
|
|
1201
|
+
async doStreamWithResponsesApi(options, isRetry) {
|
|
1202
|
+
const client = await this.getOpenAIClient(isRetry);
|
|
1203
|
+
const input = this.convertPromptForResponses(options.prompt);
|
|
1204
|
+
const tools = this.convertToolsForResponses(options.tools);
|
|
1205
|
+
const instructions = this.extractSystemInstructions(options.prompt);
|
|
1206
|
+
const openaiModel = this.config.openaiModel || "gpt-5-codex";
|
|
1207
|
+
const maxTokens = options.maxOutputTokens || this.config.maxTokens || 8192;
|
|
1208
|
+
const requestBody = {
|
|
1209
|
+
model: openaiModel,
|
|
1210
|
+
input,
|
|
1211
|
+
instructions,
|
|
1212
|
+
tools,
|
|
1213
|
+
max_output_tokens: maxTokens,
|
|
1214
|
+
temperature: options.temperature,
|
|
1215
|
+
top_p: options.topP,
|
|
1216
|
+
store: false,
|
|
1217
|
+
stream: true
|
|
1218
|
+
};
|
|
1219
|
+
const self = this;
|
|
1220
|
+
const stream = new ReadableStream({
|
|
1221
|
+
start: async (controller) => {
|
|
1222
|
+
const toolCalls = {};
|
|
1223
|
+
const usage = {
|
|
1224
|
+
inputTokens: 0,
|
|
1225
|
+
outputTokens: 0,
|
|
1226
|
+
totalTokens: 0
|
|
1227
|
+
};
|
|
1228
|
+
let finishReason = "unknown";
|
|
1229
|
+
let textStarted = false;
|
|
1230
|
+
const textId = "text-0";
|
|
1231
|
+
try {
|
|
1232
|
+
const openaiStream = await client.responses.create({
|
|
1233
|
+
...requestBody,
|
|
1234
|
+
stream: true
|
|
1235
|
+
});
|
|
1236
|
+
controller.enqueue({ type: "stream-start", warnings: [] });
|
|
1237
|
+
for await (const event of openaiStream) {
|
|
1238
|
+
if (event.type === "response.created") {
|
|
1239
|
+
controller.enqueue({
|
|
1240
|
+
type: "response-metadata",
|
|
1241
|
+
id: event.response.id,
|
|
1242
|
+
modelId: event.response.model
|
|
1243
|
+
});
|
|
1244
|
+
} else if (event.type === "response.output_item.added") {
|
|
1245
|
+
if (event.item.type === "function_call") {
|
|
1246
|
+
const outputIndex = event.output_index;
|
|
1247
|
+
const callId = event.item.call_id;
|
|
1248
|
+
toolCalls[outputIndex] = {
|
|
1249
|
+
callId,
|
|
1250
|
+
name: event.item.name,
|
|
1251
|
+
arguments: ""
|
|
1252
|
+
};
|
|
1253
|
+
controller.enqueue({
|
|
1254
|
+
type: "tool-input-start",
|
|
1255
|
+
id: callId,
|
|
1256
|
+
toolName: event.item.name
|
|
1257
|
+
});
|
|
1258
|
+
}
|
|
1259
|
+
} else if (event.type === "response.output_text.delta") {
|
|
1260
|
+
if (!textStarted) {
|
|
1261
|
+
controller.enqueue({ type: "text-start", id: textId });
|
|
1262
|
+
textStarted = true;
|
|
1263
|
+
}
|
|
1264
|
+
controller.enqueue({
|
|
1265
|
+
type: "text-delta",
|
|
1266
|
+
id: textId,
|
|
1267
|
+
delta: event.delta
|
|
1268
|
+
});
|
|
1269
|
+
} else if (event.type === "response.function_call_arguments.delta") {
|
|
1270
|
+
const outputIndex = event.output_index;
|
|
1271
|
+
const tc = toolCalls[outputIndex];
|
|
1272
|
+
if (tc) {
|
|
1273
|
+
tc.arguments += event.delta;
|
|
1274
|
+
controller.enqueue({
|
|
1275
|
+
type: "tool-input-delta",
|
|
1276
|
+
id: tc.callId,
|
|
1277
|
+
delta: event.delta
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
} else if (event.type === "response.function_call_arguments.done") {
|
|
1281
|
+
const outputIndex = event.output_index;
|
|
1282
|
+
const tc = toolCalls[outputIndex];
|
|
1283
|
+
if (tc) {
|
|
1284
|
+
tc.arguments = event.arguments;
|
|
1285
|
+
}
|
|
1286
|
+
} else if (event.type === "response.completed") {
|
|
1287
|
+
const hasToolCalls2 = Object.keys(toolCalls).length > 0;
|
|
1288
|
+
finishReason = self.convertResponsesStatus(event.response.status, hasToolCalls2);
|
|
1289
|
+
if (event.response.usage) {
|
|
1290
|
+
usage.inputTokens = event.response.usage.input_tokens || 0;
|
|
1291
|
+
usage.outputTokens = event.response.usage.output_tokens || 0;
|
|
1292
|
+
usage.totalTokens = event.response.usage.total_tokens || 0;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
if (textStarted) {
|
|
1297
|
+
controller.enqueue({ type: "text-end", id: textId });
|
|
1298
|
+
}
|
|
1299
|
+
const hasToolCalls = Object.keys(toolCalls).length > 0;
|
|
1300
|
+
if (hasToolCalls && finishReason === "stop") {
|
|
1301
|
+
finishReason = "tool-calls";
|
|
1302
|
+
}
|
|
1303
|
+
for (const tc of Object.values(toolCalls)) {
|
|
1304
|
+
controller.enqueue({ type: "tool-input-end", id: tc.callId });
|
|
1305
|
+
controller.enqueue({
|
|
1306
|
+
type: "tool-call",
|
|
1307
|
+
toolCallId: tc.callId,
|
|
1308
|
+
toolName: tc.name,
|
|
1309
|
+
input: tc.arguments || "{}"
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
controller.enqueue({ type: "finish", finishReason, usage });
|
|
1313
|
+
controller.close();
|
|
1314
|
+
} catch (error) {
|
|
1315
|
+
if (!isRetry && self.isTokenError(error)) {
|
|
1316
|
+
self.directAccessClient.invalidateToken();
|
|
1317
|
+
controller.enqueue({
|
|
1318
|
+
type: "error",
|
|
1319
|
+
error: new GitLabError({ message: "TOKEN_REFRESH_NEEDED", cause: error })
|
|
1320
|
+
});
|
|
1321
|
+
controller.close();
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
if (error instanceof OpenAI.APIError) {
|
|
1325
|
+
controller.enqueue({
|
|
1326
|
+
type: "error",
|
|
1327
|
+
error: new GitLabError({
|
|
1328
|
+
message: `OpenAI API error: ${error.message}`,
|
|
1329
|
+
cause: error
|
|
1330
|
+
})
|
|
1331
|
+
});
|
|
1332
|
+
} else {
|
|
1333
|
+
controller.enqueue({ type: "error", error });
|
|
1334
|
+
}
|
|
1335
|
+
controller.close();
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
});
|
|
1339
|
+
return { stream, request: { body: requestBody } };
|
|
1340
|
+
}
|
|
1341
|
+
};
|
|
1342
|
+
|
|
599
1343
|
// src/gitlab-oauth-types.ts
|
|
600
1344
|
var BUNDLED_CLIENT_ID = "36f2a70cddeb5a0889d4fd8295c241b7e9848e89cf9e599d0eed2d8e5350fbf5";
|
|
601
1345
|
var GITLAB_COM_URL = "https://gitlab.com";
|
|
@@ -758,16 +1502,6 @@ var GitLabOAuthManager = class {
|
|
|
758
1502
|
}
|
|
759
1503
|
};
|
|
760
1504
|
|
|
761
|
-
// src/model-mappings.ts
|
|
762
|
-
var MODEL_ID_TO_ANTHROPIC_MODEL = {
|
|
763
|
-
"duo-chat-opus-4-5": "claude-opus-4-5-20251101",
|
|
764
|
-
"duo-chat-sonnet-4-5": "claude-sonnet-4-5-20250929",
|
|
765
|
-
"duo-chat-haiku-4-5": "claude-haiku-4-5-20251001"
|
|
766
|
-
};
|
|
767
|
-
function getAnthropicModelForModelId(modelId) {
|
|
768
|
-
return MODEL_ID_TO_ANTHROPIC_MODEL[modelId];
|
|
769
|
-
}
|
|
770
|
-
|
|
771
1505
|
// src/gitlab-provider.ts
|
|
772
1506
|
import * as fs from "fs";
|
|
773
1507
|
import * as path from "path";
|
|
@@ -903,21 +1637,44 @@ function createGitLab(options = {}) {
|
|
|
903
1637
|
getApiKey().catch(() => {
|
|
904
1638
|
});
|
|
905
1639
|
const createAgenticChatModel = (modelId, agenticOptions) => {
|
|
1640
|
+
const mapping = getModelMapping(modelId);
|
|
1641
|
+
if (!mapping) {
|
|
1642
|
+
throw new GitLabError({
|
|
1643
|
+
message: `Unknown model ID: ${modelId}. Model must be registered in MODEL_MAPPINGS.`
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
if (agenticOptions?.providerModel) {
|
|
1647
|
+
const validModels = getValidModelsForProvider(mapping.provider);
|
|
1648
|
+
if (!validModels.includes(agenticOptions.providerModel)) {
|
|
1649
|
+
throw new GitLabError({
|
|
1650
|
+
message: `Invalid providerModel '${agenticOptions.providerModel}' for provider '${mapping.provider}'. Valid models: ${validModels.join(", ")}`
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
906
1654
|
const featureFlags = {
|
|
907
1655
|
DuoAgentPlatformNext: true,
|
|
908
1656
|
...options.featureFlags,
|
|
909
1657
|
...agenticOptions?.featureFlags
|
|
910
1658
|
};
|
|
911
|
-
|
|
1659
|
+
const baseConfig = {
|
|
912
1660
|
provider: `${providerName}.agentic`,
|
|
913
1661
|
instanceUrl,
|
|
914
1662
|
getHeaders,
|
|
915
1663
|
refreshApiKey,
|
|
916
1664
|
fetch: options.fetch,
|
|
917
|
-
anthropicModel: agenticOptions?.anthropicModel ?? getAnthropicModelForModelId(modelId),
|
|
918
1665
|
maxTokens: agenticOptions?.maxTokens,
|
|
919
1666
|
featureFlags,
|
|
920
1667
|
aiGatewayUrl: options.aiGatewayUrl
|
|
1668
|
+
};
|
|
1669
|
+
if (mapping.provider === "openai") {
|
|
1670
|
+
return new GitLabOpenAILanguageModel(modelId, {
|
|
1671
|
+
...baseConfig,
|
|
1672
|
+
openaiModel: agenticOptions?.providerModel ?? mapping.model
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
return new GitLabAnthropicLanguageModel(modelId, {
|
|
1676
|
+
...baseConfig,
|
|
1677
|
+
anthropicModel: agenticOptions?.providerModel ?? mapping.model
|
|
921
1678
|
});
|
|
922
1679
|
};
|
|
923
1680
|
const createDefaultModel = (modelId) => {
|
|
@@ -1216,17 +1973,25 @@ export {
|
|
|
1216
1973
|
BUNDLED_CLIENT_ID,
|
|
1217
1974
|
DEFAULT_AI_GATEWAY_URL,
|
|
1218
1975
|
GITLAB_COM_URL,
|
|
1219
|
-
|
|
1976
|
+
GitLabAnthropicLanguageModel,
|
|
1220
1977
|
GitLabDirectAccessClient,
|
|
1221
1978
|
GitLabError,
|
|
1222
1979
|
GitLabOAuthManager,
|
|
1980
|
+
GitLabOpenAILanguageModel,
|
|
1223
1981
|
GitLabProjectCache,
|
|
1224
1982
|
GitLabProjectDetector,
|
|
1225
1983
|
MODEL_ID_TO_ANTHROPIC_MODEL,
|
|
1984
|
+
MODEL_MAPPINGS,
|
|
1226
1985
|
OAUTH_SCOPES,
|
|
1227
1986
|
TOKEN_EXPIRY_SKEW_MS,
|
|
1228
1987
|
createGitLab,
|
|
1229
1988
|
getAnthropicModelForModelId,
|
|
1230
|
-
|
|
1989
|
+
getModelMapping,
|
|
1990
|
+
getOpenAIApiType,
|
|
1991
|
+
getOpenAIModelForModelId,
|
|
1992
|
+
getProviderForModelId,
|
|
1993
|
+
getValidModelsForProvider,
|
|
1994
|
+
gitlab,
|
|
1995
|
+
isResponsesApiModel
|
|
1231
1996
|
};
|
|
1232
1997
|
//# sourceMappingURL=index.mjs.map
|