pi-gitlab-duo 0.1.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/README.md +61 -0
- package/index.ts +402 -0
- package/package.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# pi-gitlab-duo
|
|
2
|
+
|
|
3
|
+
GitLab Duo provider extension for [pi](https://github.com/badlogic/pi-mono).
|
|
4
|
+
|
|
5
|
+
Provides access to GitLab Duo AI models (Claude and GPT) through GitLab's AI Gateway.
|
|
6
|
+
|
|
7
|
+
## Models
|
|
8
|
+
|
|
9
|
+
| Model ID | Backend |
|
|
10
|
+
|----------|---------|
|
|
11
|
+
| `duo-chat-opus-4-5` | Claude Opus 4.5 |
|
|
12
|
+
| `duo-chat-sonnet-4-5` | Claude Sonnet 4.5 |
|
|
13
|
+
| `duo-chat-haiku-4-5` | Claude Haiku 4.5 |
|
|
14
|
+
| `duo-chat-gpt-5-1` | GPT-5.1 |
|
|
15
|
+
| `duo-chat-gpt-5-mini` | GPT-5 Mini |
|
|
16
|
+
| `duo-chat-gpt-5-codex` | GPT-5 Codex |
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pi install npm:pi-gitlab-duo
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Authentication
|
|
25
|
+
|
|
26
|
+
### OAuth (Recommended)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pi
|
|
30
|
+
/login gitlab-duo
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This will open GitLab's OAuth flow. After authorizing, copy the callback URL and paste it when prompted.
|
|
34
|
+
|
|
35
|
+
### Personal Access Token
|
|
36
|
+
|
|
37
|
+
Set the `GITLAB_TOKEN` environment variable:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
export GITLAB_TOKEN=glpat-xxxxxxxxxxxxxxxxxxxx
|
|
41
|
+
pi --provider gitlab-duo --model duo-chat-sonnet-4-5
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Interactive mode
|
|
48
|
+
pi --provider gitlab-duo --model duo-chat-sonnet-4-5
|
|
49
|
+
|
|
50
|
+
# Print mode
|
|
51
|
+
pi --provider gitlab-duo --model duo-chat-sonnet-4-5 -p "explain git rebase"
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Requirements
|
|
55
|
+
|
|
56
|
+
- GitLab Duo subscription (Duo Pro or Duo Enterprise)
|
|
57
|
+
- pi >= 0.49.0
|
|
58
|
+
|
|
59
|
+
## License
|
|
60
|
+
|
|
61
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GitLab Duo Provider Extension for pi
|
|
3
|
+
*
|
|
4
|
+
* Provides access to GitLab Duo AI models (Claude and GPT) through GitLab's AI Gateway.
|
|
5
|
+
* Delegates to pi-ai's built-in Anthropic and OpenAI streaming implementations.
|
|
6
|
+
*
|
|
7
|
+
* Installation:
|
|
8
|
+
* pi install npm:pi-gitlab-duo
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* # With OAuth (run /login gitlab-duo first)
|
|
12
|
+
* pi --provider gitlab-duo --model duo-chat-sonnet-4-5
|
|
13
|
+
*
|
|
14
|
+
* # With PAT
|
|
15
|
+
* GITLAB_TOKEN=glpat-... pi --provider gitlab-duo --model duo-chat-sonnet-4-5
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
type Api,
|
|
20
|
+
type AssistantMessageEventStream,
|
|
21
|
+
type Context,
|
|
22
|
+
createAssistantMessageEventStream,
|
|
23
|
+
type Model,
|
|
24
|
+
type OAuthCredentials,
|
|
25
|
+
type OAuthLoginCallbacks,
|
|
26
|
+
type SimpleStreamOptions,
|
|
27
|
+
streamSimple,
|
|
28
|
+
} from "@mariozechner/pi-ai";
|
|
29
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// Constants
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
const GITLAB_COM_URL = "https://gitlab.com";
|
|
36
|
+
const AI_GATEWAY_URL = "https://cloud.gitlab.com";
|
|
37
|
+
const ANTHROPIC_PROXY_URL = `${AI_GATEWAY_URL}/ai/v1/proxy/anthropic/`;
|
|
38
|
+
const OPENAI_PROXY_URL = `${AI_GATEWAY_URL}/ai/v1/proxy/openai/v1`;
|
|
39
|
+
|
|
40
|
+
// Bundled OAuth client ID for gitlab.com (from opencode-gitlab-auth, registered with localhost redirect)
|
|
41
|
+
const BUNDLED_CLIENT_ID = "1d89f9fdb23ee96d4e603201f6861dab6e143c5c3c00469a018a2d94bdc03d4e";
|
|
42
|
+
const OAUTH_SCOPES = ["api"];
|
|
43
|
+
const REDIRECT_URI = "http://127.0.0.1:8080/callback";
|
|
44
|
+
|
|
45
|
+
// Direct access token cache (25 min, tokens expire after 30 min)
|
|
46
|
+
const DIRECT_ACCESS_TTL = 25 * 60 * 1000;
|
|
47
|
+
|
|
48
|
+
// Model mappings: duo model ID -> backend config
|
|
49
|
+
const MODEL_MAPPINGS: Record<
|
|
50
|
+
string,
|
|
51
|
+
{ api: "anthropic-messages" | "openai-completions"; backendModel: string; baseUrl: string }
|
|
52
|
+
> = {
|
|
53
|
+
"duo-chat-opus-4-5": {
|
|
54
|
+
api: "anthropic-messages",
|
|
55
|
+
backendModel: "claude-opus-4-5-20251101",
|
|
56
|
+
baseUrl: ANTHROPIC_PROXY_URL,
|
|
57
|
+
},
|
|
58
|
+
"duo-chat-sonnet-4-5": {
|
|
59
|
+
api: "anthropic-messages",
|
|
60
|
+
backendModel: "claude-sonnet-4-5-20250929",
|
|
61
|
+
baseUrl: ANTHROPIC_PROXY_URL,
|
|
62
|
+
},
|
|
63
|
+
"duo-chat-haiku-4-5": {
|
|
64
|
+
api: "anthropic-messages",
|
|
65
|
+
backendModel: "claude-haiku-4-5-20251001",
|
|
66
|
+
baseUrl: ANTHROPIC_PROXY_URL,
|
|
67
|
+
},
|
|
68
|
+
"duo-chat-gpt-5-1": { api: "openai-completions", backendModel: "gpt-5.1-2025-11-13", baseUrl: OPENAI_PROXY_URL },
|
|
69
|
+
"duo-chat-gpt-5-mini": {
|
|
70
|
+
api: "openai-completions",
|
|
71
|
+
backendModel: "gpt-5-mini-2025-08-07",
|
|
72
|
+
baseUrl: OPENAI_PROXY_URL,
|
|
73
|
+
},
|
|
74
|
+
"duo-chat-gpt-5-codex": { api: "openai-completions", backendModel: "gpt-5-codex", baseUrl: OPENAI_PROXY_URL },
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// =============================================================================
|
|
78
|
+
// Direct Access Token Cache
|
|
79
|
+
// =============================================================================
|
|
80
|
+
|
|
81
|
+
interface DirectAccessToken {
|
|
82
|
+
token: string;
|
|
83
|
+
headers: Record<string, string>;
|
|
84
|
+
expiresAt: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
let cachedDirectAccess: DirectAccessToken | null = null;
|
|
88
|
+
|
|
89
|
+
async function getDirectAccessToken(gitlabAccessToken: string): Promise<DirectAccessToken> {
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
if (cachedDirectAccess && cachedDirectAccess.expiresAt > now) {
|
|
92
|
+
return cachedDirectAccess;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const url = `${GITLAB_COM_URL}/api/v4/ai/third_party_agents/direct_access`;
|
|
96
|
+
const response = await fetch(url, {
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: {
|
|
99
|
+
Authorization: `Bearer ${gitlabAccessToken}`,
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
},
|
|
102
|
+
body: JSON.stringify({ feature_flags: { DuoAgentPlatformNext: true } }),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
if (!response.ok) {
|
|
106
|
+
const errorText = await response.text();
|
|
107
|
+
if (response.status === 403) {
|
|
108
|
+
throw new Error(
|
|
109
|
+
`GitLab Duo access denied. Ensure GitLab Duo is enabled for your account. Error: ${errorText}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`Failed to get direct access token: ${response.status} ${errorText}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const data = (await response.json()) as { token: string; headers: Record<string, string> };
|
|
116
|
+
cachedDirectAccess = {
|
|
117
|
+
token: data.token,
|
|
118
|
+
headers: data.headers,
|
|
119
|
+
expiresAt: now + DIRECT_ACCESS_TTL,
|
|
120
|
+
};
|
|
121
|
+
return cachedDirectAccess;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function invalidateDirectAccessToken() {
|
|
125
|
+
cachedDirectAccess = null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// =============================================================================
|
|
129
|
+
// OAuth Implementation
|
|
130
|
+
// =============================================================================
|
|
131
|
+
|
|
132
|
+
async function generatePKCE(): Promise<{ verifier: string; challenge: string }> {
|
|
133
|
+
const array = new Uint8Array(32);
|
|
134
|
+
crypto.getRandomValues(array);
|
|
135
|
+
const verifier = btoa(String.fromCharCode(...array))
|
|
136
|
+
.replace(/\+/g, "-")
|
|
137
|
+
.replace(/\//g, "_")
|
|
138
|
+
.replace(/=+$/, "");
|
|
139
|
+
|
|
140
|
+
const encoder = new TextEncoder();
|
|
141
|
+
const data = encoder.encode(verifier);
|
|
142
|
+
const hash = await crypto.subtle.digest("SHA-256", data);
|
|
143
|
+
const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
|
|
144
|
+
.replace(/\+/g, "-")
|
|
145
|
+
.replace(/\//g, "_")
|
|
146
|
+
.replace(/=+$/, "");
|
|
147
|
+
|
|
148
|
+
return { verifier, challenge };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function loginGitLab(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {
|
|
152
|
+
const { verifier, challenge } = await generatePKCE();
|
|
153
|
+
|
|
154
|
+
const authParams = new URLSearchParams({
|
|
155
|
+
client_id: BUNDLED_CLIENT_ID,
|
|
156
|
+
redirect_uri: REDIRECT_URI,
|
|
157
|
+
response_type: "code",
|
|
158
|
+
scope: OAUTH_SCOPES.join(" "),
|
|
159
|
+
code_challenge: challenge,
|
|
160
|
+
code_challenge_method: "S256",
|
|
161
|
+
state: crypto.randomUUID(),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
callbacks.onAuth({ url: `${GITLAB_COM_URL}/oauth/authorize?${authParams.toString()}` });
|
|
165
|
+
const callbackUrl = await callbacks.onPrompt({ message: "Paste the callback URL:" });
|
|
166
|
+
|
|
167
|
+
const urlObj = new URL(callbackUrl);
|
|
168
|
+
const code = urlObj.searchParams.get("code");
|
|
169
|
+
if (!code) throw new Error("No authorization code found in callback URL");
|
|
170
|
+
|
|
171
|
+
const tokenResponse = await fetch(`${GITLAB_COM_URL}/oauth/token`, {
|
|
172
|
+
method: "POST",
|
|
173
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
174
|
+
body: new URLSearchParams({
|
|
175
|
+
client_id: BUNDLED_CLIENT_ID,
|
|
176
|
+
grant_type: "authorization_code",
|
|
177
|
+
code,
|
|
178
|
+
code_verifier: verifier,
|
|
179
|
+
redirect_uri: REDIRECT_URI,
|
|
180
|
+
}).toString(),
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
if (!tokenResponse.ok) throw new Error(`Token exchange failed: ${await tokenResponse.text()}`);
|
|
184
|
+
|
|
185
|
+
const data = (await tokenResponse.json()) as {
|
|
186
|
+
access_token: string;
|
|
187
|
+
refresh_token: string;
|
|
188
|
+
expires_in: number;
|
|
189
|
+
created_at: number;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
invalidateDirectAccessToken();
|
|
193
|
+
return {
|
|
194
|
+
refresh: data.refresh_token,
|
|
195
|
+
access: data.access_token,
|
|
196
|
+
expires: (data.created_at + data.expires_in) * 1000 - 5 * 60 * 1000,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function refreshGitLabToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {
|
|
201
|
+
const response = await fetch(`${GITLAB_COM_URL}/oauth/token`, {
|
|
202
|
+
method: "POST",
|
|
203
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
204
|
+
body: new URLSearchParams({
|
|
205
|
+
client_id: BUNDLED_CLIENT_ID,
|
|
206
|
+
grant_type: "refresh_token",
|
|
207
|
+
refresh_token: credentials.refresh,
|
|
208
|
+
}).toString(),
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
if (!response.ok) throw new Error(`Token refresh failed: ${await response.text()}`);
|
|
212
|
+
|
|
213
|
+
const data = (await response.json()) as {
|
|
214
|
+
access_token: string;
|
|
215
|
+
refresh_token: string;
|
|
216
|
+
expires_in: number;
|
|
217
|
+
created_at: number;
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
invalidateDirectAccessToken();
|
|
221
|
+
return {
|
|
222
|
+
refresh: data.refresh_token,
|
|
223
|
+
access: data.access_token,
|
|
224
|
+
expires: (data.created_at + data.expires_in) * 1000 - 5 * 60 * 1000,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// =============================================================================
|
|
229
|
+
// Main Stream Function - Delegates to pi-ai's built-in implementations
|
|
230
|
+
// =============================================================================
|
|
231
|
+
|
|
232
|
+
function streamGitLabDuo(
|
|
233
|
+
model: Model<Api>,
|
|
234
|
+
context: Context,
|
|
235
|
+
options?: SimpleStreamOptions,
|
|
236
|
+
): AssistantMessageEventStream {
|
|
237
|
+
const stream = createAssistantMessageEventStream();
|
|
238
|
+
|
|
239
|
+
(async () => {
|
|
240
|
+
try {
|
|
241
|
+
const gitlabAccessToken = options?.apiKey;
|
|
242
|
+
if (!gitlabAccessToken) {
|
|
243
|
+
throw new Error("No GitLab access token. Run /login gitlab-duo or set GITLAB_TOKEN");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const mapping = MODEL_MAPPINGS[model.id];
|
|
247
|
+
if (!mapping) throw new Error(`Unknown model: ${model.id}`);
|
|
248
|
+
|
|
249
|
+
// Get direct access token (cached)
|
|
250
|
+
const directAccess = await getDirectAccessToken(gitlabAccessToken);
|
|
251
|
+
|
|
252
|
+
// Create a proxy model that uses the backend API
|
|
253
|
+
const proxyModel: Model<typeof mapping.api> = {
|
|
254
|
+
...model,
|
|
255
|
+
id: mapping.backendModel,
|
|
256
|
+
api: mapping.api,
|
|
257
|
+
baseUrl: mapping.baseUrl,
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
// Merge GitLab headers with Authorization bearer token
|
|
261
|
+
const headers = {
|
|
262
|
+
...directAccess.headers,
|
|
263
|
+
Authorization: `Bearer ${directAccess.token}`,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
// Delegate to pi-ai's built-in streaming
|
|
267
|
+
const innerStream = streamSimple(proxyModel, context, {
|
|
268
|
+
...options,
|
|
269
|
+
apiKey: "gitlab-duo", // Dummy value to pass validation
|
|
270
|
+
headers,
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Forward all events
|
|
274
|
+
for await (const event of innerStream) {
|
|
275
|
+
// Patch the model info back to gitlab-duo
|
|
276
|
+
if ("partial" in event && event.partial) {
|
|
277
|
+
event.partial.api = model.api;
|
|
278
|
+
event.partial.provider = model.provider;
|
|
279
|
+
event.partial.model = model.id;
|
|
280
|
+
}
|
|
281
|
+
if ("message" in event && event.message) {
|
|
282
|
+
event.message.api = model.api;
|
|
283
|
+
event.message.provider = model.provider;
|
|
284
|
+
event.message.model = model.id;
|
|
285
|
+
}
|
|
286
|
+
if ("error" in event && event.error) {
|
|
287
|
+
event.error.api = model.api;
|
|
288
|
+
event.error.provider = model.provider;
|
|
289
|
+
event.error.model = model.id;
|
|
290
|
+
}
|
|
291
|
+
stream.push(event);
|
|
292
|
+
}
|
|
293
|
+
stream.end();
|
|
294
|
+
} catch (error) {
|
|
295
|
+
stream.push({
|
|
296
|
+
type: "error",
|
|
297
|
+
reason: "error",
|
|
298
|
+
error: {
|
|
299
|
+
role: "assistant",
|
|
300
|
+
content: [],
|
|
301
|
+
api: model.api,
|
|
302
|
+
provider: model.provider,
|
|
303
|
+
model: model.id,
|
|
304
|
+
usage: {
|
|
305
|
+
input: 0,
|
|
306
|
+
output: 0,
|
|
307
|
+
cacheRead: 0,
|
|
308
|
+
cacheWrite: 0,
|
|
309
|
+
totalTokens: 0,
|
|
310
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
|
|
311
|
+
},
|
|
312
|
+
stopReason: "error",
|
|
313
|
+
errorMessage: error instanceof Error ? error.message : String(error),
|
|
314
|
+
timestamp: Date.now(),
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
stream.end();
|
|
318
|
+
}
|
|
319
|
+
})();
|
|
320
|
+
|
|
321
|
+
return stream;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// =============================================================================
|
|
325
|
+
// Extension Entry Point
|
|
326
|
+
// =============================================================================
|
|
327
|
+
|
|
328
|
+
export default function (pi: ExtensionAPI) {
|
|
329
|
+
pi.registerProvider("gitlab-duo", {
|
|
330
|
+
baseUrl: AI_GATEWAY_URL,
|
|
331
|
+
apiKey: "GITLAB_TOKEN",
|
|
332
|
+
api: "gitlab-duo-api",
|
|
333
|
+
|
|
334
|
+
models: [
|
|
335
|
+
// Anthropic models
|
|
336
|
+
{
|
|
337
|
+
id: "duo-chat-opus-4-5",
|
|
338
|
+
name: "GitLab Duo Claude Opus 4.5",
|
|
339
|
+
reasoning: false,
|
|
340
|
+
input: ["text"],
|
|
341
|
+
cost: { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
|
342
|
+
contextWindow: 200000,
|
|
343
|
+
maxTokens: 32000,
|
|
344
|
+
},
|
|
345
|
+
{
|
|
346
|
+
id: "duo-chat-sonnet-4-5",
|
|
347
|
+
name: "GitLab Duo Claude Sonnet 4.5",
|
|
348
|
+
reasoning: false,
|
|
349
|
+
input: ["text"],
|
|
350
|
+
cost: { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
|
351
|
+
contextWindow: 200000,
|
|
352
|
+
maxTokens: 16384,
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
id: "duo-chat-haiku-4-5",
|
|
356
|
+
name: "GitLab Duo Claude Haiku 4.5",
|
|
357
|
+
reasoning: false,
|
|
358
|
+
input: ["text"],
|
|
359
|
+
cost: { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
|
360
|
+
contextWindow: 200000,
|
|
361
|
+
maxTokens: 8192,
|
|
362
|
+
},
|
|
363
|
+
// OpenAI models
|
|
364
|
+
{
|
|
365
|
+
id: "duo-chat-gpt-5-1",
|
|
366
|
+
name: "GitLab Duo GPT-5.1",
|
|
367
|
+
reasoning: false,
|
|
368
|
+
input: ["text"],
|
|
369
|
+
cost: { input: 2.5, output: 10, cacheRead: 0, cacheWrite: 0 },
|
|
370
|
+
contextWindow: 128000,
|
|
371
|
+
maxTokens: 16384,
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
id: "duo-chat-gpt-5-mini",
|
|
375
|
+
name: "GitLab Duo GPT-5 Mini",
|
|
376
|
+
reasoning: false,
|
|
377
|
+
input: ["text"],
|
|
378
|
+
cost: { input: 0.15, output: 0.6, cacheRead: 0, cacheWrite: 0 },
|
|
379
|
+
contextWindow: 128000,
|
|
380
|
+
maxTokens: 16384,
|
|
381
|
+
},
|
|
382
|
+
{
|
|
383
|
+
id: "duo-chat-gpt-5-codex",
|
|
384
|
+
name: "GitLab Duo GPT-5 Codex",
|
|
385
|
+
reasoning: false,
|
|
386
|
+
input: ["text"],
|
|
387
|
+
cost: { input: 2.5, output: 10, cacheRead: 0, cacheWrite: 0 },
|
|
388
|
+
contextWindow: 128000,
|
|
389
|
+
maxTokens: 16384,
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
|
|
393
|
+
oauth: {
|
|
394
|
+
name: "GitLab Duo",
|
|
395
|
+
login: loginGitLab,
|
|
396
|
+
refreshToken: refreshGitLabToken,
|
|
397
|
+
getApiKey: (cred) => cred.access,
|
|
398
|
+
},
|
|
399
|
+
|
|
400
|
+
streamSimple: streamGitLabDuo,
|
|
401
|
+
});
|
|
402
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-gitlab-duo",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "GitLab Duo provider extension for pi",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Mario Zechner",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/badlogic/pi-gitlab-duo.git"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"pi",
|
|
14
|
+
"gitlab",
|
|
15
|
+
"gitlab-duo",
|
|
16
|
+
"ai",
|
|
17
|
+
"llm",
|
|
18
|
+
"claude",
|
|
19
|
+
"gpt"
|
|
20
|
+
],
|
|
21
|
+
"pi": {
|
|
22
|
+
"extensions": [
|
|
23
|
+
"./index.ts"
|
|
24
|
+
]
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"index.ts",
|
|
28
|
+
"README.md"
|
|
29
|
+
],
|
|
30
|
+
"peerDependencies": {
|
|
31
|
+
"@mariozechner/pi-ai": ">=0.49.0",
|
|
32
|
+
"@mariozechner/pi-coding-agent": ">=0.49.0"
|
|
33
|
+
}
|
|
34
|
+
}
|