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