@f5-sales-demo/pi-ai 19.51.2
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 +1997 -0
- package/README.md +1160 -0
- package/package.json +135 -0
- package/src/api-registry.ts +95 -0
- package/src/auth-storage.ts +2694 -0
- package/src/cli.ts +493 -0
- package/src/index.ts +42 -0
- package/src/model-cache.ts +97 -0
- package/src/model-manager.ts +349 -0
- package/src/model-thinking.ts +561 -0
- package/src/models.json +49439 -0
- package/src/models.json.d.ts +9 -0
- package/src/models.ts +56 -0
- package/src/prompts/turn-aborted-guidance.md +4 -0
- package/src/provider-details.ts +81 -0
- package/src/provider-models/descriptors.ts +285 -0
- package/src/provider-models/google.ts +90 -0
- package/src/provider-models/index.ts +4 -0
- package/src/provider-models/openai-compat.ts +2074 -0
- package/src/provider-models/special.ts +106 -0
- package/src/providers/amazon-bedrock.ts +706 -0
- package/src/providers/anthropic.ts +1682 -0
- package/src/providers/azure-openai-responses.ts +391 -0
- package/src/providers/cursor/gen/agent_pb.ts +15274 -0
- package/src/providers/cursor/proto/agent.proto +3526 -0
- package/src/providers/cursor/proto/buf.gen.yaml +6 -0
- package/src/providers/cursor/proto/buf.yaml +17 -0
- package/src/providers/cursor.ts +2218 -0
- package/src/providers/github-copilot-headers.ts +140 -0
- package/src/providers/gitlab-duo.ts +381 -0
- package/src/providers/google-gemini-cli.ts +1133 -0
- package/src/providers/google-shared.ts +354 -0
- package/src/providers/google-vertex.ts +436 -0
- package/src/providers/google.ts +381 -0
- package/src/providers/kimi.ts +151 -0
- package/src/providers/openai-codex/constants.ts +43 -0
- package/src/providers/openai-codex/request-transformer.ts +158 -0
- package/src/providers/openai-codex/response-handler.ts +81 -0
- package/src/providers/openai-codex-responses.ts +2345 -0
- package/src/providers/openai-completions-compat.ts +159 -0
- package/src/providers/openai-completions.ts +1290 -0
- package/src/providers/openai-responses-shared.ts +452 -0
- package/src/providers/openai-responses.ts +519 -0
- package/src/providers/register-builtins.ts +329 -0
- package/src/providers/synthetic.ts +154 -0
- package/src/providers/transform-messages.ts +234 -0
- package/src/rate-limit-utils.ts +84 -0
- package/src/stream.ts +728 -0
- package/src/types.ts +546 -0
- package/src/usage/claude.ts +337 -0
- package/src/usage/gemini.ts +248 -0
- package/src/usage/github-copilot.ts +421 -0
- package/src/usage/google-antigravity.ts +200 -0
- package/src/usage/kimi.ts +286 -0
- package/src/usage/minimax-code.ts +31 -0
- package/src/usage/openai-codex.ts +387 -0
- package/src/usage/zai.ts +247 -0
- package/src/usage.ts +130 -0
- package/src/utils/abort.ts +36 -0
- package/src/utils/anthropic-auth.ts +293 -0
- package/src/utils/discovery/antigravity.ts +261 -0
- package/src/utils/discovery/codex.ts +371 -0
- package/src/utils/discovery/cursor.ts +306 -0
- package/src/utils/discovery/gemini.ts +248 -0
- package/src/utils/discovery/index.ts +5 -0
- package/src/utils/discovery/openai-compatible.ts +224 -0
- package/src/utils/event-stream.ts +209 -0
- package/src/utils/http-inspector.ts +165 -0
- package/src/utils/idle-iterator.ts +176 -0
- package/src/utils/json-parse.ts +28 -0
- package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
- package/src/utils/oauth/anthropic.ts +134 -0
- package/src/utils/oauth/api-key-validation.ts +92 -0
- package/src/utils/oauth/callback-server.ts +276 -0
- package/src/utils/oauth/cerebras.ts +59 -0
- package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
- package/src/utils/oauth/cursor.ts +157 -0
- package/src/utils/oauth/github-copilot.ts +358 -0
- package/src/utils/oauth/gitlab-duo.ts +123 -0
- package/src/utils/oauth/google-antigravity.ts +275 -0
- package/src/utils/oauth/google-gemini-cli.ts +334 -0
- package/src/utils/oauth/huggingface.ts +62 -0
- package/src/utils/oauth/index.ts +512 -0
- package/src/utils/oauth/kagi.ts +47 -0
- package/src/utils/oauth/kilo.ts +87 -0
- package/src/utils/oauth/kimi.ts +251 -0
- package/src/utils/oauth/litellm.ts +81 -0
- package/src/utils/oauth/lm-studio.ts +40 -0
- package/src/utils/oauth/minimax-code.ts +78 -0
- package/src/utils/oauth/moonshot.ts +59 -0
- package/src/utils/oauth/nanogpt.ts +51 -0
- package/src/utils/oauth/nvidia.ts +70 -0
- package/src/utils/oauth/oauth.html +199 -0
- package/src/utils/oauth/ollama.ts +47 -0
- package/src/utils/oauth/openai-codex.ts +190 -0
- package/src/utils/oauth/opencode.ts +49 -0
- package/src/utils/oauth/parallel.ts +46 -0
- package/src/utils/oauth/perplexity.ts +200 -0
- package/src/utils/oauth/pkce.ts +18 -0
- package/src/utils/oauth/qianfan.ts +58 -0
- package/src/utils/oauth/qwen-portal.ts +60 -0
- package/src/utils/oauth/synthetic.ts +60 -0
- package/src/utils/oauth/tavily.ts +46 -0
- package/src/utils/oauth/together.ts +59 -0
- package/src/utils/oauth/types.ts +89 -0
- package/src/utils/oauth/venice.ts +59 -0
- package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
- package/src/utils/oauth/vllm.ts +40 -0
- package/src/utils/oauth/xiaomi.ts +88 -0
- package/src/utils/oauth/zai.ts +60 -0
- package/src/utils/oauth/zenmux.ts +51 -0
- package/src/utils/overflow.ts +134 -0
- package/src/utils/retry-after.ts +110 -0
- package/src/utils/retry.ts +93 -0
- package/src/utils/schema/CONSTRAINTS.md +160 -0
- package/src/utils/schema/adapt.ts +20 -0
- package/src/utils/schema/compatibility.ts +397 -0
- package/src/utils/schema/dereference.ts +93 -0
- package/src/utils/schema/equality.ts +93 -0
- package/src/utils/schema/fields.ts +147 -0
- package/src/utils/schema/index.ts +9 -0
- package/src/utils/schema/normalize-cca.ts +479 -0
- package/src/utils/schema/sanitize-google.ts +212 -0
- package/src/utils/schema/strict-mode.ts +385 -0
- package/src/utils/schema/types.ts +5 -0
- package/src/utils/tool-choice.ts +81 -0
- package/src/utils/validation.ts +664 -0
- package/src/utils.ts +147 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import type { Message } from "../types";
|
|
2
|
+
import { getGitHubCopilotBaseUrl, parseGitHubCopilotApiKey } from "../utils/oauth/github-copilot";
|
|
3
|
+
/**
|
|
4
|
+
* Infer whether the current request to Copilot is user-initiated or agent-initiated.
|
|
5
|
+
* Accepts `unknown[]` because providers may pass pre-converted message shapes.
|
|
6
|
+
*/
|
|
7
|
+
export type CopilotInitiator = "user" | "agent";
|
|
8
|
+
export type CopilotPremiumRequests = number;
|
|
9
|
+
export type CopilotDynamicHeaders = {
|
|
10
|
+
headers: Record<string, string>;
|
|
11
|
+
initiator: CopilotInitiator;
|
|
12
|
+
premiumRequests: CopilotPremiumRequests;
|
|
13
|
+
};
|
|
14
|
+
export function resolveGitHubCopilotBaseUrl(
|
|
15
|
+
baseUrl: string | undefined,
|
|
16
|
+
apiKey: string | undefined,
|
|
17
|
+
): string | undefined {
|
|
18
|
+
if (!apiKey) return baseUrl;
|
|
19
|
+
const { enterpriseUrl } = parseGitHubCopilotApiKey(apiKey);
|
|
20
|
+
if (!enterpriseUrl) return baseUrl;
|
|
21
|
+
if (baseUrl && !baseUrl.includes("githubcopilot.com")) return baseUrl;
|
|
22
|
+
return getGitHubCopilotBaseUrl(enterpriseUrl);
|
|
23
|
+
}
|
|
24
|
+
export function inferCopilotInitiator(messages: unknown[]): CopilotInitiator {
|
|
25
|
+
if (messages.length === 0) return "user";
|
|
26
|
+
|
|
27
|
+
const last = messages[messages.length - 1] as Record<string, unknown>;
|
|
28
|
+
const attribution = last.attribution;
|
|
29
|
+
if (typeof attribution === "string") {
|
|
30
|
+
const normalizedAttribution = attribution.trim().toLowerCase();
|
|
31
|
+
if (normalizedAttribution === "user" || normalizedAttribution === "agent") {
|
|
32
|
+
return normalizedAttribution;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const role = last.role as string | undefined;
|
|
37
|
+
if (!role) return "user";
|
|
38
|
+
|
|
39
|
+
if (role !== "user") return "agent";
|
|
40
|
+
|
|
41
|
+
// Check if last content block is a tool_result (Anthropic-converted shape)
|
|
42
|
+
const content = last.content;
|
|
43
|
+
if (Array.isArray(content) && content.length > 0) {
|
|
44
|
+
const lastBlock = content[content.length - 1] as Record<string, unknown>;
|
|
45
|
+
if (lastBlock.type === "tool_result") {
|
|
46
|
+
return "agent";
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return "user";
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Check whether any message in the conversation contains image content. */
|
|
54
|
+
export function hasCopilotVisionInput(messages: Message[]): boolean {
|
|
55
|
+
return messages.some(msg => {
|
|
56
|
+
if (msg.role === "user" && Array.isArray(msg.content)) {
|
|
57
|
+
return msg.content.some(c => c.type === "image");
|
|
58
|
+
}
|
|
59
|
+
if (msg.role === "toolResult" && Array.isArray(msg.content)) {
|
|
60
|
+
return msg.content.some(c => c.type === "image");
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolve an explicitly configured Copilot initiator header, if present.
|
|
68
|
+
* Handles case-insensitive X-Initiator keys and returns the last valid value.
|
|
69
|
+
*/
|
|
70
|
+
export function getCopilotInitiatorOverride(headers: Record<string, string> | undefined): CopilotInitiator | undefined {
|
|
71
|
+
if (!headers) return undefined;
|
|
72
|
+
|
|
73
|
+
let override: CopilotInitiator | undefined;
|
|
74
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
75
|
+
if (key.toLowerCase() !== "x-initiator") continue;
|
|
76
|
+
const normalized = value.trim().toLowerCase();
|
|
77
|
+
if (normalized === "user" || normalized === "agent") {
|
|
78
|
+
override = normalized;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return override;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type CopilotPlanTier = "free" | "paid";
|
|
86
|
+
|
|
87
|
+
function normalizeCopilotPlanTier(planTier: string | undefined): CopilotPlanTier {
|
|
88
|
+
if (planTier === "paid") return "paid";
|
|
89
|
+
return "free";
|
|
90
|
+
}
|
|
91
|
+
export function getCopilotPremiumMultiplier(premiumMultiplier: number | undefined, planTier?: string): number {
|
|
92
|
+
const normalizedMultiplier = premiumMultiplier ?? 1;
|
|
93
|
+
if (normalizeCopilotPlanTier(planTier) === "free" && normalizedMultiplier === 0) {
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
return normalizedMultiplier;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function getCopilotPremiumRequests(params: {
|
|
100
|
+
initiator: CopilotInitiator;
|
|
101
|
+
premiumMultiplier?: number;
|
|
102
|
+
planTier?: string;
|
|
103
|
+
}): CopilotPremiumRequests {
|
|
104
|
+
if (params.initiator === "agent") return 0;
|
|
105
|
+
return getCopilotPremiumMultiplier(params.premiumMultiplier, params.planTier);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Build dynamic Copilot headers that vary per-request.
|
|
110
|
+
* Static headers (User-Agent, Editor-Version, etc.) come from model.headers.
|
|
111
|
+
*/
|
|
112
|
+
export function buildCopilotDynamicHeaders(params: {
|
|
113
|
+
messages: unknown[];
|
|
114
|
+
hasImages: boolean;
|
|
115
|
+
premiumMultiplier?: number;
|
|
116
|
+
headers?: Record<string, string>;
|
|
117
|
+
initiatorOverride?: CopilotInitiator;
|
|
118
|
+
planTier?: string;
|
|
119
|
+
}): CopilotDynamicHeaders {
|
|
120
|
+
const initiator =
|
|
121
|
+
params.initiatorOverride ?? getCopilotInitiatorOverride(params.headers) ?? inferCopilotInitiator(params.messages);
|
|
122
|
+
const headers: Record<string, string> = {
|
|
123
|
+
"X-Initiator": initiator,
|
|
124
|
+
"Openai-Intent": "conversation-edits",
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (params.hasImages) {
|
|
128
|
+
headers["Copilot-Vision-Request"] = "true";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
headers,
|
|
133
|
+
initiator,
|
|
134
|
+
premiumRequests: getCopilotPremiumRequests({
|
|
135
|
+
initiator,
|
|
136
|
+
premiumMultiplier: params.premiumMultiplier,
|
|
137
|
+
planTier: params.planTier,
|
|
138
|
+
}),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { ANTHROPIC_THINKING, mapAnthropicToolChoice } from "../stream";
|
|
2
|
+
import type { Api, Context, Model, SimpleStreamOptions } from "../types";
|
|
3
|
+
import { AssistantMessageEventStream } from "../utils/event-stream";
|
|
4
|
+
import { streamAnthropic } from "./anthropic";
|
|
5
|
+
import type { OpenAICompletionsOptions } from "./openai-completions";
|
|
6
|
+
import { streamOpenAICompletions } from "./openai-completions";
|
|
7
|
+
import type { OpenAIResponsesOptions } from "./openai-responses";
|
|
8
|
+
import { streamOpenAIResponses } from "./openai-responses";
|
|
9
|
+
|
|
10
|
+
const GITLAB_COM_URL = "https://gitlab.com";
|
|
11
|
+
const AI_GATEWAY_URL = "https://cloud.gitlab.com";
|
|
12
|
+
const ANTHROPIC_PROXY_URL = `${AI_GATEWAY_URL}/ai/v1/proxy/anthropic/`;
|
|
13
|
+
const OPENAI_PROXY_URL = `${AI_GATEWAY_URL}/ai/v1/proxy/openai/v1`;
|
|
14
|
+
const DIRECT_ACCESS_TTL_MS = 25 * 60 * 1000;
|
|
15
|
+
|
|
16
|
+
type GitLabProvider = "anthropic" | "openai";
|
|
17
|
+
type GitLabOpenAIApiType = "chat" | "responses";
|
|
18
|
+
|
|
19
|
+
export type GitLabModelMapping = {
|
|
20
|
+
provider: GitLabProvider;
|
|
21
|
+
model: string;
|
|
22
|
+
openaiApiType?: GitLabOpenAIApiType;
|
|
23
|
+
name: string;
|
|
24
|
+
reasoning: boolean;
|
|
25
|
+
input: ("text" | "image")[];
|
|
26
|
+
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
|
|
27
|
+
contextWindow: number;
|
|
28
|
+
maxTokens: number;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export const MODEL_MAPPINGS: Record<string, GitLabModelMapping> = {
|
|
32
|
+
"duo-chat-opus-4-6": {
|
|
33
|
+
provider: "anthropic",
|
|
34
|
+
model: "claude-opus-4-6",
|
|
35
|
+
name: "Duo Chat Opus 4.6",
|
|
36
|
+
reasoning: true,
|
|
37
|
+
input: ["text", "image"],
|
|
38
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
39
|
+
contextWindow: 200000,
|
|
40
|
+
maxTokens: 64000,
|
|
41
|
+
},
|
|
42
|
+
"duo-chat-sonnet-4-6": {
|
|
43
|
+
provider: "anthropic",
|
|
44
|
+
model: "claude-sonnet-4-6",
|
|
45
|
+
name: "Duo Chat Sonnet 4.6",
|
|
46
|
+
reasoning: true,
|
|
47
|
+
input: ["text", "image"],
|
|
48
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
49
|
+
contextWindow: 200000,
|
|
50
|
+
maxTokens: 64000,
|
|
51
|
+
},
|
|
52
|
+
"duo-chat-opus-4-5": {
|
|
53
|
+
provider: "anthropic",
|
|
54
|
+
model: "claude-opus-4-5-20251101",
|
|
55
|
+
name: "Duo Chat Opus 4.5",
|
|
56
|
+
reasoning: true,
|
|
57
|
+
input: ["text", "image"],
|
|
58
|
+
cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
|
59
|
+
contextWindow: 200000,
|
|
60
|
+
maxTokens: 64000,
|
|
61
|
+
},
|
|
62
|
+
"duo-chat-sonnet-4-5": {
|
|
63
|
+
provider: "anthropic",
|
|
64
|
+
model: "claude-sonnet-4-5-20250929",
|
|
65
|
+
name: "Duo Chat Sonnet 4.5",
|
|
66
|
+
reasoning: true,
|
|
67
|
+
input: ["text", "image"],
|
|
68
|
+
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
69
|
+
contextWindow: 200000,
|
|
70
|
+
maxTokens: 64000,
|
|
71
|
+
},
|
|
72
|
+
"duo-chat-haiku-4-5": {
|
|
73
|
+
provider: "anthropic",
|
|
74
|
+
model: "claude-haiku-4-5-20251001",
|
|
75
|
+
name: "Duo Chat Haiku 4.5",
|
|
76
|
+
reasoning: true,
|
|
77
|
+
input: ["text", "image"],
|
|
78
|
+
cost: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
|
79
|
+
contextWindow: 200000,
|
|
80
|
+
maxTokens: 64000,
|
|
81
|
+
},
|
|
82
|
+
"duo-chat-gpt-5-1": {
|
|
83
|
+
provider: "openai",
|
|
84
|
+
model: "gpt-5.1-2025-11-13",
|
|
85
|
+
openaiApiType: "chat",
|
|
86
|
+
name: "Duo Chat GPT-5.1",
|
|
87
|
+
reasoning: true,
|
|
88
|
+
input: ["text", "image"],
|
|
89
|
+
cost: { input: 2.5, output: 10, cacheRead: 0, cacheWrite: 0 },
|
|
90
|
+
contextWindow: 128000,
|
|
91
|
+
maxTokens: 16384,
|
|
92
|
+
},
|
|
93
|
+
"duo-chat-gpt-5-2": {
|
|
94
|
+
provider: "openai",
|
|
95
|
+
model: "gpt-5.2-2025-12-11",
|
|
96
|
+
openaiApiType: "chat",
|
|
97
|
+
name: "Duo Chat GPT-5.2",
|
|
98
|
+
reasoning: true,
|
|
99
|
+
input: ["text", "image"],
|
|
100
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
101
|
+
contextWindow: 128000,
|
|
102
|
+
maxTokens: 16384,
|
|
103
|
+
},
|
|
104
|
+
"duo-chat-gpt-5-mini": {
|
|
105
|
+
provider: "openai",
|
|
106
|
+
model: "gpt-5-mini-2025-08-07",
|
|
107
|
+
openaiApiType: "chat",
|
|
108
|
+
name: "Duo Chat GPT-5 Mini",
|
|
109
|
+
reasoning: true,
|
|
110
|
+
input: ["text", "image"],
|
|
111
|
+
cost: { input: 0.15, output: 0.6, cacheRead: 0, cacheWrite: 0 },
|
|
112
|
+
contextWindow: 128000,
|
|
113
|
+
maxTokens: 16384,
|
|
114
|
+
},
|
|
115
|
+
"duo-chat-gpt-5-codex": {
|
|
116
|
+
provider: "openai",
|
|
117
|
+
model: "gpt-5-codex",
|
|
118
|
+
openaiApiType: "responses",
|
|
119
|
+
name: "Duo Chat GPT-5 Codex",
|
|
120
|
+
reasoning: true,
|
|
121
|
+
input: ["text", "image"],
|
|
122
|
+
cost: { input: 2.5, output: 10, cacheRead: 0, cacheWrite: 0 },
|
|
123
|
+
contextWindow: 272000,
|
|
124
|
+
maxTokens: 128000,
|
|
125
|
+
},
|
|
126
|
+
"duo-chat-gpt-5-2-codex": {
|
|
127
|
+
provider: "openai",
|
|
128
|
+
model: "gpt-5.2-codex",
|
|
129
|
+
openaiApiType: "responses",
|
|
130
|
+
name: "Duo Chat GPT-5.2 Codex",
|
|
131
|
+
reasoning: true,
|
|
132
|
+
input: ["text", "image"],
|
|
133
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
134
|
+
contextWindow: 272000,
|
|
135
|
+
maxTokens: 128000,
|
|
136
|
+
},
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export function getModelMapping(modelId: string): GitLabModelMapping | undefined {
|
|
140
|
+
const direct = MODEL_MAPPINGS[modelId];
|
|
141
|
+
if (direct) return direct;
|
|
142
|
+
|
|
143
|
+
// Support canonical model IDs (e.g. "gpt-5-codex", "claude-sonnet-4-5-20250929")
|
|
144
|
+
// in addition to Duo aliases (e.g. "duo-chat-gpt-5-codex").
|
|
145
|
+
return Object.values(MODEL_MAPPINGS).find(mapping => mapping.model === modelId);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getGitLabDuoModels(): Model<Api>[] {
|
|
149
|
+
return Object.entries(MODEL_MAPPINGS).map(([id, mapping]) => ({
|
|
150
|
+
id,
|
|
151
|
+
name: mapping.name,
|
|
152
|
+
api:
|
|
153
|
+
mapping.provider === "anthropic"
|
|
154
|
+
? "anthropic-messages"
|
|
155
|
+
: mapping.openaiApiType === "responses"
|
|
156
|
+
? "openai-responses"
|
|
157
|
+
: "openai-completions",
|
|
158
|
+
provider: "gitlab-duo",
|
|
159
|
+
baseUrl: mapping.provider === "anthropic" ? ANTHROPIC_PROXY_URL : OPENAI_PROXY_URL,
|
|
160
|
+
reasoning: mapping.reasoning,
|
|
161
|
+
input: [...mapping.input],
|
|
162
|
+
cost: { ...mapping.cost },
|
|
163
|
+
contextWindow: mapping.contextWindow,
|
|
164
|
+
maxTokens: mapping.maxTokens,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
interface DirectAccessToken {
|
|
169
|
+
token: string;
|
|
170
|
+
headers: Record<string, string>;
|
|
171
|
+
expiresAt: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const directAccessCache = new Map<string, DirectAccessToken>();
|
|
175
|
+
|
|
176
|
+
async function getDirectAccessToken(gitlabAccessToken: string): Promise<DirectAccessToken> {
|
|
177
|
+
const cached = directAccessCache.get(gitlabAccessToken);
|
|
178
|
+
if (cached && cached.expiresAt > Date.now()) {
|
|
179
|
+
return cached;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const response = await fetch(`${GITLAB_COM_URL}/api/v4/ai/third_party_agents/direct_access`, {
|
|
183
|
+
method: "POST",
|
|
184
|
+
headers: {
|
|
185
|
+
Authorization: `Bearer ${gitlabAccessToken}`,
|
|
186
|
+
"Content-Type": "application/json",
|
|
187
|
+
},
|
|
188
|
+
body: JSON.stringify({
|
|
189
|
+
feature_flags: { DuoAgentPlatformNext: true },
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
const detail = await response.text();
|
|
195
|
+
if (response.status === 403) {
|
|
196
|
+
throw new Error(`GitLab Duo access denied. Ensure Duo is enabled for this account. ${detail}`);
|
|
197
|
+
}
|
|
198
|
+
throw new Error(`Failed to get GitLab Duo direct access token: ${response.status} ${detail}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const payload = (await response.json()) as { token?: string; headers?: Record<string, string> };
|
|
202
|
+
if (!payload.token || typeof payload.token !== "string") {
|
|
203
|
+
throw new Error("GitLab Duo direct access response missing token");
|
|
204
|
+
}
|
|
205
|
+
if (!payload.headers || typeof payload.headers !== "object") {
|
|
206
|
+
throw new Error("GitLab Duo direct access response missing headers");
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const token: DirectAccessToken = {
|
|
210
|
+
token: payload.token,
|
|
211
|
+
headers: payload.headers,
|
|
212
|
+
expiresAt: Date.now() + DIRECT_ACCESS_TTL_MS,
|
|
213
|
+
};
|
|
214
|
+
directAccessCache.set(gitlabAccessToken, token);
|
|
215
|
+
return token;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getErrorMessage(model: Model<Api>, err: unknown) {
|
|
219
|
+
return {
|
|
220
|
+
role: "assistant" as const,
|
|
221
|
+
content: [{ type: "text" as const, text: err instanceof Error ? err.message : String(err) }],
|
|
222
|
+
api: model.api,
|
|
223
|
+
provider: model.provider,
|
|
224
|
+
model: model.id,
|
|
225
|
+
usage: {
|
|
226
|
+
input: 0,
|
|
227
|
+
output: 0,
|
|
228
|
+
cacheRead: 0,
|
|
229
|
+
cacheWrite: 0,
|
|
230
|
+
totalTokens: 0,
|
|
231
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
232
|
+
},
|
|
233
|
+
stopReason: "error" as const,
|
|
234
|
+
timestamp: Date.now(),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export function clearGitLabDuoDirectAccessCache(): void {
|
|
239
|
+
directAccessCache.clear();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function isGitLabDuoModel(model: Model<Api>): boolean {
|
|
243
|
+
return model.provider === "gitlab-duo";
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function streamGitLabDuo(
|
|
247
|
+
model: Model<Api>,
|
|
248
|
+
context: Context,
|
|
249
|
+
options?: SimpleStreamOptions,
|
|
250
|
+
): AssistantMessageEventStream {
|
|
251
|
+
const stream = new AssistantMessageEventStream();
|
|
252
|
+
|
|
253
|
+
(async () => {
|
|
254
|
+
try {
|
|
255
|
+
if (!options?.apiKey) {
|
|
256
|
+
throw new Error("Missing GitLab access token. Run /login gitlab-duo or set GITLAB_TOKEN.");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const mapping = getModelMapping(model.id);
|
|
260
|
+
if (!mapping) {
|
|
261
|
+
throw new Error(`Unsupported GitLab Duo model: ${model.id}`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const directAccess = await getDirectAccessToken(options.apiKey);
|
|
265
|
+
const headers = {
|
|
266
|
+
...directAccess.headers,
|
|
267
|
+
...options.headers,
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const reasoningEffort = options.reasoning;
|
|
271
|
+
|
|
272
|
+
const inner =
|
|
273
|
+
mapping.provider === "anthropic"
|
|
274
|
+
? streamAnthropic(
|
|
275
|
+
{
|
|
276
|
+
...model,
|
|
277
|
+
id: mapping.model,
|
|
278
|
+
api: "anthropic-messages",
|
|
279
|
+
baseUrl: ANTHROPIC_PROXY_URL,
|
|
280
|
+
} as Model<"anthropic-messages">,
|
|
281
|
+
context,
|
|
282
|
+
{
|
|
283
|
+
apiKey: directAccess.token,
|
|
284
|
+
isOAuth: true,
|
|
285
|
+
temperature: options.temperature,
|
|
286
|
+
topP: options.topP,
|
|
287
|
+
topK: options.topK,
|
|
288
|
+
minP: options.minP,
|
|
289
|
+
presencePenalty: options.presencePenalty,
|
|
290
|
+
repetitionPenalty: options.repetitionPenalty,
|
|
291
|
+
maxTokens: options.maxTokens ?? Math.min(model.maxTokens, 32000),
|
|
292
|
+
signal: options.signal,
|
|
293
|
+
cacheRetention: options.cacheRetention,
|
|
294
|
+
headers,
|
|
295
|
+
maxRetryDelayMs: options.maxRetryDelayMs,
|
|
296
|
+
metadata: options.metadata,
|
|
297
|
+
sessionId: options.sessionId,
|
|
298
|
+
providerSessionState: options.providerSessionState,
|
|
299
|
+
onPayload: options.onPayload,
|
|
300
|
+
thinkingEnabled: Boolean(reasoningEffort) && model.reasoning,
|
|
301
|
+
thinkingBudgetTokens: reasoningEffort
|
|
302
|
+
? (options.thinkingBudgets?.[reasoningEffort] ?? ANTHROPIC_THINKING[reasoningEffort])
|
|
303
|
+
: undefined,
|
|
304
|
+
reasoning: reasoningEffort,
|
|
305
|
+
toolChoice: mapAnthropicToolChoice(options.toolChoice),
|
|
306
|
+
},
|
|
307
|
+
)
|
|
308
|
+
: mapping.openaiApiType === "responses"
|
|
309
|
+
? streamOpenAIResponses(
|
|
310
|
+
{
|
|
311
|
+
...model,
|
|
312
|
+
id: mapping.model,
|
|
313
|
+
api: "openai-responses",
|
|
314
|
+
baseUrl: OPENAI_PROXY_URL,
|
|
315
|
+
} as Model<"openai-responses">,
|
|
316
|
+
context,
|
|
317
|
+
{
|
|
318
|
+
apiKey: directAccess.token,
|
|
319
|
+
temperature: options.temperature,
|
|
320
|
+
topP: options.topP,
|
|
321
|
+
topK: options.topK,
|
|
322
|
+
minP: options.minP,
|
|
323
|
+
presencePenalty: options.presencePenalty,
|
|
324
|
+
repetitionPenalty: options.repetitionPenalty,
|
|
325
|
+
maxTokens: options.maxTokens ?? model.maxTokens,
|
|
326
|
+
signal: options.signal,
|
|
327
|
+
cacheRetention: options.cacheRetention,
|
|
328
|
+
headers,
|
|
329
|
+
maxRetryDelayMs: options.maxRetryDelayMs,
|
|
330
|
+
metadata: options.metadata,
|
|
331
|
+
sessionId: options.sessionId,
|
|
332
|
+
providerSessionState: options.providerSessionState,
|
|
333
|
+
onPayload: options.onPayload,
|
|
334
|
+
reasoning: reasoningEffort,
|
|
335
|
+
toolChoice: options.toolChoice,
|
|
336
|
+
} satisfies OpenAIResponsesOptions,
|
|
337
|
+
)
|
|
338
|
+
: streamOpenAICompletions(
|
|
339
|
+
{
|
|
340
|
+
...model,
|
|
341
|
+
id: mapping.model,
|
|
342
|
+
api: "openai-completions",
|
|
343
|
+
baseUrl: OPENAI_PROXY_URL,
|
|
344
|
+
} as Model<"openai-completions">,
|
|
345
|
+
context,
|
|
346
|
+
{
|
|
347
|
+
apiKey: directAccess.token,
|
|
348
|
+
temperature: options.temperature,
|
|
349
|
+
topP: options.topP,
|
|
350
|
+
topK: options.topK,
|
|
351
|
+
minP: options.minP,
|
|
352
|
+
presencePenalty: options.presencePenalty,
|
|
353
|
+
repetitionPenalty: options.repetitionPenalty,
|
|
354
|
+
maxTokens: options.maxTokens ?? model.maxTokens,
|
|
355
|
+
signal: options.signal,
|
|
356
|
+
cacheRetention: options.cacheRetention,
|
|
357
|
+
headers,
|
|
358
|
+
maxRetryDelayMs: options.maxRetryDelayMs,
|
|
359
|
+
metadata: options.metadata,
|
|
360
|
+
sessionId: options.sessionId,
|
|
361
|
+
providerSessionState: options.providerSessionState,
|
|
362
|
+
onPayload: options.onPayload,
|
|
363
|
+
reasoning: reasoningEffort,
|
|
364
|
+
toolChoice: options.toolChoice,
|
|
365
|
+
} satisfies OpenAICompletionsOptions,
|
|
366
|
+
);
|
|
367
|
+
|
|
368
|
+
for await (const event of inner) {
|
|
369
|
+
stream.push(event);
|
|
370
|
+
}
|
|
371
|
+
} catch (err) {
|
|
372
|
+
stream.push({
|
|
373
|
+
type: "error",
|
|
374
|
+
reason: "error",
|
|
375
|
+
error: getErrorMessage(model, err),
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
})();
|
|
379
|
+
|
|
380
|
+
return stream;
|
|
381
|
+
}
|