pi-yandex-bridge 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 +50 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +318 -0
- package/dist/index.js.map +1 -0
- package/index.ts +406 -0
- package/package.json +32 -0
- package/tsconfig.json +17 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# pi-yandex-bridge
|
|
2
|
+
|
|
3
|
+
Pi Coding Agent provider bridge for Yandex Cloud AI (YandexGPT).
|
|
4
|
+
|
|
5
|
+
## Models
|
|
6
|
+
|
|
7
|
+
| Model | Context | Max output |
|
|
8
|
+
| ----------------- | ------- | ---------- |
|
|
9
|
+
| YandexGPT Pro 5.1 | 128k | 8k |
|
|
10
|
+
| YandexGPT Pro | 128k | 8k |
|
|
11
|
+
| YandexGPT Lite | 32k | 4k |
|
|
12
|
+
|
|
13
|
+
Unknown models discovered via the API are displayed as `slug {last4ofFolderId}`.
|
|
14
|
+
|
|
15
|
+
## Auth: OAuth (default)
|
|
16
|
+
|
|
17
|
+
### 1. Find your Yandex Cloud folder ID
|
|
18
|
+
|
|
19
|
+
In the [Yandex Cloud console](https://console.yandex.cloud), select your folder. The ID is shown in the URL and on the folder overview page (looks like `b1gk28...`).
|
|
20
|
+
|
|
21
|
+
### 2. Log in
|
|
22
|
+
|
|
23
|
+
Run `/yalogin` in Pi. A browser window will open automatically — authorize the app, and the token is captured without any pasting. Pi then prompts for your folder ID.
|
|
24
|
+
|
|
25
|
+
**IAM tokens expire after 12 hours** and are refreshed automatically using the stored OAuth token.
|
|
26
|
+
|
|
27
|
+
To skip the browser flow, set env vars before starting Pi:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
export YANDEX_OAUTH_TOKEN="y0_AgAAAA..."
|
|
31
|
+
export YANDEX_FOLDER_ID="b1g..."
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Auth: Static API key
|
|
35
|
+
|
|
36
|
+
Set both env vars before starting Pi — no `auth.json` entry needed:
|
|
37
|
+
|
|
38
|
+
```sh
|
|
39
|
+
export YANDEX_API_KEY="your-api-key"
|
|
40
|
+
export YANDEX_FOLDER_ID="your-folder-id"
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
You can generate an API key in the Yandex Cloud console under **Service accounts → your account → API keys**. The key is shown only once, so copy it immediately.
|
|
44
|
+
|
|
45
|
+
Env vars also work for OAuth to skip interactive prompts:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
export YANDEX_OAUTH_TOKEN="y0_AgAAAA..."
|
|
49
|
+
export YANDEX_FOLDER_ID="b1g..."
|
|
50
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Coding Agent — Yandex Provider Bridge
|
|
3
|
+
*
|
|
4
|
+
* Auth (OAuth — default):
|
|
5
|
+
* Run /yalogin to authorize. A browser window opens automatically,
|
|
6
|
+
* the token is captured via a local callback server on port 7890.
|
|
7
|
+
*
|
|
8
|
+
* YANDEX_OAUTH_TOKEN env var skips the browser flow entirely.
|
|
9
|
+
* YANDEX_FOLDER_ID env var skips the folder ID prompt.
|
|
10
|
+
*
|
|
11
|
+
* Auth (static API key — opt-in):
|
|
12
|
+
* Set both YANDEX_API_KEY and YANDEX_FOLDER_ID env vars.
|
|
13
|
+
*/
|
|
14
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
15
|
+
export default function (pi: ExtensionAPI): Promise<void>;
|
|
16
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAQH,OAAO,KAAK,EACX,YAAY,EAGZ,MAAM,iCAAiC,CAAC;AA4RzC,yBAA+B,EAAE,EAAE,YAAY,iBAwF9C"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Coding Agent — Yandex Provider Bridge
|
|
3
|
+
*
|
|
4
|
+
* Auth (OAuth — default):
|
|
5
|
+
* Run /yalogin to authorize. A browser window opens automatically,
|
|
6
|
+
* the token is captured via a local callback server on port 7890.
|
|
7
|
+
*
|
|
8
|
+
* YANDEX_OAUTH_TOKEN env var skips the browser flow entirely.
|
|
9
|
+
* YANDEX_FOLDER_ID env var skips the folder ID prompt.
|
|
10
|
+
*
|
|
11
|
+
* Auth (static API key — opt-in):
|
|
12
|
+
* Set both YANDEX_API_KEY and YANDEX_FOLDER_ID env vars.
|
|
13
|
+
*/
|
|
14
|
+
import { createServer } from "http";
|
|
15
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
import { join } from "path";
|
|
18
|
+
import { spawnSync } from "child_process";
|
|
19
|
+
// ─── constants ────────────────────────────────────────────────────────────────
|
|
20
|
+
const IAM_TOKEN_URL = "https://iam.api.cloud.yandex.net/iam/v1/tokens";
|
|
21
|
+
const AI_BASE_URL = "https://ai.api.cloud.yandex.net/v1";
|
|
22
|
+
const AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json");
|
|
23
|
+
const OAUTH_CLIENT_ID = "0414b7213b22435fa65051f64270584f";
|
|
24
|
+
const OAUTH_CALLBACK_PORT = 7890;
|
|
25
|
+
const OAUTH_REDIRECT_URI = `http://localhost:${OAUTH_CALLBACK_PORT}/callback`;
|
|
26
|
+
const OAUTH_URL = `https://oauth.yandex.ru/authorize?response_type=token` +
|
|
27
|
+
`&client_id=${OAUTH_CLIENT_ID}` +
|
|
28
|
+
`&redirect_uri=${encodeURIComponent(OAUTH_REDIRECT_URI)}`;
|
|
29
|
+
// ─── model catalogue ──────────────────────────────────────────────────────────
|
|
30
|
+
const KNOWN_MODELS = [
|
|
31
|
+
{
|
|
32
|
+
slug: "yandexgpt-5.1",
|
|
33
|
+
name: "YandexGPT Pro 5.1",
|
|
34
|
+
contextWindow: 128_000,
|
|
35
|
+
maxTokens: 8_192,
|
|
36
|
+
cost: { input: 8.8, output: 8.8, cacheRead: 0, cacheWrite: 0 },
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
slug: "yandexgpt",
|
|
40
|
+
name: "YandexGPT Pro",
|
|
41
|
+
contextWindow: 128_000,
|
|
42
|
+
maxTokens: 8_192,
|
|
43
|
+
cost: { input: 8.8, output: 8.8, cacheRead: 0, cacheWrite: 0 },
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
slug: "yandexgpt-lite",
|
|
47
|
+
name: "YandexGPT Lite",
|
|
48
|
+
contextWindow: 32_000,
|
|
49
|
+
maxTokens: 4_096,
|
|
50
|
+
cost: { input: 2.2, output: 2.2, cacheRead: 0, cacheWrite: 0 },
|
|
51
|
+
},
|
|
52
|
+
];
|
|
53
|
+
/** Turns `gpt://<folderId>/<slug>/latest` into a readable display name.
|
|
54
|
+
* Known slugs get their canonical name; unknown ones get `slug {last4ofFolderId}`. */
|
|
55
|
+
function prettyModelName(id) {
|
|
56
|
+
const match = id.match(/^gpt:\/\/([^/]+)\/(.+?)(?:\/latest)?$/);
|
|
57
|
+
if (!match)
|
|
58
|
+
return id;
|
|
59
|
+
const [, folderId, slug] = match;
|
|
60
|
+
const known = KNOWN_MODELS.find((m) => m.slug === slug);
|
|
61
|
+
if (known)
|
|
62
|
+
return known.name;
|
|
63
|
+
return `${slug} {${folderId.slice(-4)}}`;
|
|
64
|
+
}
|
|
65
|
+
function openBrowser(url) {
|
|
66
|
+
const cmd = process.platform === "win32"
|
|
67
|
+
? "start"
|
|
68
|
+
: process.platform === "darwin"
|
|
69
|
+
? "open"
|
|
70
|
+
: "xdg-open";
|
|
71
|
+
spawnSync(cmd, [url], { stdio: "ignore" });
|
|
72
|
+
}
|
|
73
|
+
async function exchangeOAuthForIam(oauthToken) {
|
|
74
|
+
const res = await fetch(IAM_TOKEN_URL, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "Content-Type": "application/json" },
|
|
77
|
+
body: JSON.stringify({ yandexPassportOauthToken: oauthToken }),
|
|
78
|
+
});
|
|
79
|
+
if (!res.ok) {
|
|
80
|
+
const text = await res.text().catch(() => res.statusText);
|
|
81
|
+
throw new Error(`IAM token exchange failed (${res.status}): ${text}`);
|
|
82
|
+
}
|
|
83
|
+
const data = (await res.json());
|
|
84
|
+
return {
|
|
85
|
+
token: data.iamToken,
|
|
86
|
+
expiresAt: new Date(data.expiresAt).getTime(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function buildModels(folderId) {
|
|
90
|
+
return KNOWN_MODELS.map((m) => ({
|
|
91
|
+
id: `gpt://${folderId}/${m.slug}/latest`,
|
|
92
|
+
name: m.name,
|
|
93
|
+
api: "openai-responses",
|
|
94
|
+
provider: "yandex",
|
|
95
|
+
baseUrl: AI_BASE_URL,
|
|
96
|
+
reasoning: false,
|
|
97
|
+
input: ["text"],
|
|
98
|
+
cost: m.cost,
|
|
99
|
+
contextWindow: m.contextWindow,
|
|
100
|
+
maxTokens: m.maxTokens,
|
|
101
|
+
headers: { "OpenAI-Project": folderId },
|
|
102
|
+
compat: {
|
|
103
|
+
supportsDeveloperRole: false,
|
|
104
|
+
supportsReasoningEffort: false,
|
|
105
|
+
maxTokensField: "max_tokens",
|
|
106
|
+
},
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
function readAuthJson() {
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(readFileSync(AUTH_PATH, "utf8"));
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function writeAuthJson(data) {
|
|
118
|
+
writeFileSync(AUTH_PATH, JSON.stringify(data, null, 2), "utf8");
|
|
119
|
+
}
|
|
120
|
+
// ─── OAuth local callback server ──────────────────────────────────────────────
|
|
121
|
+
const CALLBACK_HTML = `<!DOCTYPE html>
|
|
122
|
+
<html><head><meta charset="utf-8"><title>pi-yandex-bridge</title></head>
|
|
123
|
+
<body style="font-family:sans-serif;padding:2rem">
|
|
124
|
+
<p>Authorizing pi-yandex-bridge…</p>
|
|
125
|
+
<script>
|
|
126
|
+
const p = new URLSearchParams(location.hash.slice(1));
|
|
127
|
+
const t = p.get('access_token');
|
|
128
|
+
if (t) {
|
|
129
|
+
fetch('/token', { method: 'POST', body: t })
|
|
130
|
+
.then(() => { document.body.innerHTML = '<p>✓ Authorized. You can close this tab.</p>'; });
|
|
131
|
+
} else {
|
|
132
|
+
document.body.innerHTML = '<p>Error: no access_token in response.</p>';
|
|
133
|
+
}
|
|
134
|
+
</script>
|
|
135
|
+
</body></html>`;
|
|
136
|
+
function captureOAuthToken(onProgress) {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const server = createServer((req, res) => {
|
|
139
|
+
if (req.url?.startsWith("/callback")) {
|
|
140
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
141
|
+
res.end(CALLBACK_HTML);
|
|
142
|
+
}
|
|
143
|
+
else if (req.url === "/token" && req.method === "POST") {
|
|
144
|
+
let body = "";
|
|
145
|
+
req.on("data", (chunk) => (body += chunk));
|
|
146
|
+
req.on("end", () => {
|
|
147
|
+
res.writeHead(200);
|
|
148
|
+
res.end();
|
|
149
|
+
server.close();
|
|
150
|
+
resolve(body.trim());
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
res.writeHead(404);
|
|
155
|
+
res.end();
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
server.on("error", (err) => {
|
|
159
|
+
if (err.code === "EADDRINUSE") {
|
|
160
|
+
reject(new Error(`Port ${OAUTH_CALLBACK_PORT} is already in use. Stop the process using it and try again.`));
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
reject(new Error(`Local auth server error: ${err.message}`));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
const timeout = setTimeout(() => {
|
|
167
|
+
server.close();
|
|
168
|
+
reject(new Error("OAuth authorization timed out after 5 minutes."));
|
|
169
|
+
}, 5 * 60 * 1000);
|
|
170
|
+
server.listen(OAUTH_CALLBACK_PORT, "127.0.0.1", () => {
|
|
171
|
+
onProgress?.(`[yandex] Listening on port ${server.address().port}. Opening browser…`);
|
|
172
|
+
openBrowser(OAUTH_URL);
|
|
173
|
+
});
|
|
174
|
+
server.on("close", () => clearTimeout(timeout));
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
// ─── OAuth provider callbacks ─────────────────────────────────────────────────
|
|
178
|
+
async function yandexLogin(callbacks) {
|
|
179
|
+
let oauthToken = process.env.YANDEX_OAUTH_TOKEN ?? "";
|
|
180
|
+
if (!oauthToken) {
|
|
181
|
+
oauthToken = await captureOAuthToken((msg) => callbacks.onProgress?.(msg));
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
callbacks.onProgress?.("[yandex] Using YANDEX_OAUTH_TOKEN from environment.");
|
|
185
|
+
}
|
|
186
|
+
let folderId = process.env.YANDEX_FOLDER_ID ?? "";
|
|
187
|
+
if (!folderId) {
|
|
188
|
+
folderId = await callbacks.onPrompt({
|
|
189
|
+
message: "Enter your Yandex Cloud folder ID:",
|
|
190
|
+
placeholder: "b1g...",
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
callbacks.onProgress?.("[yandex] Exchanging OAuth token for IAM token…");
|
|
194
|
+
const iam = await exchangeOAuthForIam(oauthToken);
|
|
195
|
+
return {
|
|
196
|
+
refresh: oauthToken,
|
|
197
|
+
access: iam.token,
|
|
198
|
+
expires: iam.expiresAt,
|
|
199
|
+
folderId,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
async function yandexRefreshToken(credentials) {
|
|
203
|
+
const iam = await exchangeOAuthForIam(credentials.refresh);
|
|
204
|
+
return { ...credentials, access: iam.token, expires: iam.expiresAt };
|
|
205
|
+
}
|
|
206
|
+
// ─── /yalogin command ─────────────────────────────────────────────────────────
|
|
207
|
+
async function runYaLogin(ctx) {
|
|
208
|
+
try {
|
|
209
|
+
ctx.ui.notify("Opening browser for Yandex authorization…", "info");
|
|
210
|
+
const oauthToken = await captureOAuthToken();
|
|
211
|
+
let folderId = process.env.YANDEX_FOLDER_ID ?? "";
|
|
212
|
+
if (!folderId) {
|
|
213
|
+
folderId =
|
|
214
|
+
(await ctx.ui.input("Yandex Cloud folder ID", "b1g...")) ?? "";
|
|
215
|
+
}
|
|
216
|
+
if (!folderId) {
|
|
217
|
+
ctx.ui.notify("Login cancelled: folder ID is required.", "error");
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
ctx.ui.notify("Exchanging OAuth token for IAM token…", "info");
|
|
221
|
+
const iam = await exchangeOAuthForIam(oauthToken);
|
|
222
|
+
const auth = readAuthJson();
|
|
223
|
+
auth.yandex = {
|
|
224
|
+
type: "oauth",
|
|
225
|
+
refresh: oauthToken,
|
|
226
|
+
access: iam.token,
|
|
227
|
+
expires: iam.expiresAt,
|
|
228
|
+
folderId,
|
|
229
|
+
};
|
|
230
|
+
writeAuthJson(auth);
|
|
231
|
+
ctx.ui.notify("✓ Yandex credentials saved. Restart Pi to activate the models.", "info");
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
ctx.ui.notify(`Yandex login failed: ${err instanceof Error ? err.message : String(err)}`, "error");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// ─── extension entry point ────────────────────────────────────────────────────
|
|
238
|
+
export default async function (pi) {
|
|
239
|
+
const apiKey = process.env.YANDEX_API_KEY;
|
|
240
|
+
const folderId = process.env.YANDEX_FOLDER_ID;
|
|
241
|
+
// Static API key path — both env vars must be present.
|
|
242
|
+
if (apiKey && folderId) {
|
|
243
|
+
const modelIds = new Set(KNOWN_MODELS.map((m) => `gpt://${folderId}/${m.slug}/latest`));
|
|
244
|
+
try {
|
|
245
|
+
const res = await fetch(`${AI_BASE_URL}/models`, {
|
|
246
|
+
headers: {
|
|
247
|
+
Authorization: `Bearer ${apiKey}`,
|
|
248
|
+
"OpenAI-Project": folderId,
|
|
249
|
+
},
|
|
250
|
+
});
|
|
251
|
+
if (res.ok) {
|
|
252
|
+
const payload = (await res.json());
|
|
253
|
+
for (const { id } of payload.data)
|
|
254
|
+
modelIds.add(id);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
/* static list is enough */
|
|
259
|
+
}
|
|
260
|
+
const models = [...modelIds].map((id) => {
|
|
261
|
+
const slug = id.replace(/^gpt:\/\/[^/]+\//, "").replace(/\/latest$/, "");
|
|
262
|
+
const known = KNOWN_MODELS.find((m) => m.slug === slug);
|
|
263
|
+
return {
|
|
264
|
+
id,
|
|
265
|
+
name: prettyModelName(id),
|
|
266
|
+
reasoning: false,
|
|
267
|
+
input: ["text"],
|
|
268
|
+
cost: known?.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
269
|
+
contextWindow: known?.contextWindow ?? 128_000,
|
|
270
|
+
maxTokens: known?.maxTokens ?? 8_192,
|
|
271
|
+
headers: { "OpenAI-Project": folderId },
|
|
272
|
+
compat: {
|
|
273
|
+
supportsDeveloperRole: false,
|
|
274
|
+
supportsReasoningEffort: false,
|
|
275
|
+
maxTokensField: "max_tokens",
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
});
|
|
279
|
+
pi.registerProvider("yandex", {
|
|
280
|
+
name: "Yandex Cloud",
|
|
281
|
+
baseUrl: AI_BASE_URL,
|
|
282
|
+
apiKey,
|
|
283
|
+
api: "openai-responses",
|
|
284
|
+
models,
|
|
285
|
+
});
|
|
286
|
+
console.log(`[yandex] Registered ${models.length} model(s) with static API key.`);
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
// OAuth path — seed models from auth.json if folderId is already stored.
|
|
290
|
+
let storedFolderId;
|
|
291
|
+
try {
|
|
292
|
+
const auth = readAuthJson();
|
|
293
|
+
storedFolderId = auth.yandex?.folderId;
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
/* auth.json absent or unreadable */
|
|
297
|
+
}
|
|
298
|
+
pi.registerProvider("yandex", {
|
|
299
|
+
name: "Yandex Cloud",
|
|
300
|
+
baseUrl: AI_BASE_URL,
|
|
301
|
+
api: "openai-responses",
|
|
302
|
+
models: storedFolderId ? buildModels(storedFolderId) : [],
|
|
303
|
+
oauth: {
|
|
304
|
+
name: "Yandex Cloud (OAuth)",
|
|
305
|
+
login: yandexLogin,
|
|
306
|
+
refreshToken: yandexRefreshToken,
|
|
307
|
+
getApiKey: (credentials) => credentials.access,
|
|
308
|
+
modifyModels: (_, credentials) => buildModels(credentials.folderId),
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
console.log("[yandex] Registered with OAuth.");
|
|
312
|
+
}
|
|
313
|
+
pi.registerCommand("yalogin", {
|
|
314
|
+
description: "Authorize Yandex Cloud (opens browser, no pasting required)",
|
|
315
|
+
handler: async (_args, ctx) => runYaLogin(ctx),
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
package/index.ts
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pi Coding Agent — Yandex Provider Bridge
|
|
3
|
+
*
|
|
4
|
+
* Auth (OAuth — default):
|
|
5
|
+
* Run /yalogin to authorize. A browser window opens automatically,
|
|
6
|
+
* the token is captured via a local callback server on port 7890.
|
|
7
|
+
*
|
|
8
|
+
* YANDEX_OAUTH_TOKEN env var skips the browser flow entirely.
|
|
9
|
+
* YANDEX_FOLDER_ID env var skips the folder ID prompt.
|
|
10
|
+
*
|
|
11
|
+
* Auth (static API key — opt-in):
|
|
12
|
+
* Set both YANDEX_API_KEY and YANDEX_FOLDER_ID env vars.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createServer } from "http";
|
|
16
|
+
import { readFileSync, writeFileSync } from "fs";
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import { join } from "path";
|
|
19
|
+
import { spawnSync } from "child_process";
|
|
20
|
+
import type { AddressInfo } from "net";
|
|
21
|
+
import type {
|
|
22
|
+
ExtensionAPI,
|
|
23
|
+
ExtensionCommandContext,
|
|
24
|
+
ProviderConfig,
|
|
25
|
+
} from "@earendil-works/pi-coding-agent";
|
|
26
|
+
import type {
|
|
27
|
+
OAuthCredentials,
|
|
28
|
+
OAuthLoginCallbacks,
|
|
29
|
+
} from "@earendil-works/pi-ai";
|
|
30
|
+
|
|
31
|
+
// ─── constants ────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
const IAM_TOKEN_URL = "https://iam.api.cloud.yandex.net/iam/v1/tokens";
|
|
34
|
+
const AI_BASE_URL = "https://ai.api.cloud.yandex.net/v1";
|
|
35
|
+
const AUTH_PATH = join(homedir(), ".pi", "agent", "auth.json");
|
|
36
|
+
|
|
37
|
+
const OAUTH_CLIENT_ID = "0414b7213b22435fa65051f64270584f";
|
|
38
|
+
const OAUTH_CALLBACK_PORT = 7890;
|
|
39
|
+
const OAUTH_REDIRECT_URI = `http://localhost:${OAUTH_CALLBACK_PORT}/callback`;
|
|
40
|
+
const OAUTH_URL =
|
|
41
|
+
`https://oauth.yandex.ru/authorize?response_type=token` +
|
|
42
|
+
`&client_id=${OAUTH_CLIENT_ID}` +
|
|
43
|
+
`&redirect_uri=${encodeURIComponent(OAUTH_REDIRECT_URI)}`;
|
|
44
|
+
|
|
45
|
+
// ─── model catalogue ──────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
const KNOWN_MODELS = [
|
|
48
|
+
{
|
|
49
|
+
slug: "yandexgpt-5.1",
|
|
50
|
+
name: "YandexGPT Pro 5.1",
|
|
51
|
+
contextWindow: 128_000,
|
|
52
|
+
maxTokens: 8_192,
|
|
53
|
+
cost: { input: 8.8, output: 8.8, cacheRead: 0, cacheWrite: 0 },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
slug: "yandexgpt",
|
|
57
|
+
name: "YandexGPT Pro",
|
|
58
|
+
contextWindow: 128_000,
|
|
59
|
+
maxTokens: 8_192,
|
|
60
|
+
cost: { input: 8.8, output: 8.8, cacheRead: 0, cacheWrite: 0 },
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
slug: "yandexgpt-lite",
|
|
64
|
+
name: "YandexGPT Lite",
|
|
65
|
+
contextWindow: 32_000,
|
|
66
|
+
maxTokens: 4_096,
|
|
67
|
+
cost: { input: 2.2, output: 2.2, cacheRead: 0, cacheWrite: 0 },
|
|
68
|
+
},
|
|
69
|
+
] as const;
|
|
70
|
+
|
|
71
|
+
// ─── helpers ──────────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
interface IamTokenResponse {
|
|
74
|
+
iamToken: string;
|
|
75
|
+
expiresAt: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Turns `gpt://<folderId>/<slug>/latest` into a readable display name.
|
|
79
|
+
* Known slugs get their canonical name; unknown ones get `slug {last4ofFolderId}`. */
|
|
80
|
+
function prettyModelName(id: string): string {
|
|
81
|
+
const match = id.match(/^gpt:\/\/([^/]+)\/(.+?)(?:\/latest)?$/);
|
|
82
|
+
if (!match) return id;
|
|
83
|
+
const [, folderId, slug] = match;
|
|
84
|
+
const known = KNOWN_MODELS.find((m) => m.slug === slug);
|
|
85
|
+
if (known) return known.name;
|
|
86
|
+
return `${slug} {${folderId.slice(-4)}}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function openBrowser(url: string) {
|
|
90
|
+
const cmd =
|
|
91
|
+
process.platform === "win32"
|
|
92
|
+
? "start"
|
|
93
|
+
: process.platform === "darwin"
|
|
94
|
+
? "open"
|
|
95
|
+
: "xdg-open";
|
|
96
|
+
spawnSync(cmd, [url], { stdio: "ignore" });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function exchangeOAuthForIam(
|
|
100
|
+
oauthToken: string,
|
|
101
|
+
): Promise<{ token: string; expiresAt: number }> {
|
|
102
|
+
const res = await fetch(IAM_TOKEN_URL, {
|
|
103
|
+
method: "POST",
|
|
104
|
+
headers: { "Content-Type": "application/json" },
|
|
105
|
+
body: JSON.stringify({ yandexPassportOauthToken: oauthToken }),
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
if (!res.ok) {
|
|
109
|
+
const text = await res.text().catch(() => res.statusText);
|
|
110
|
+
throw new Error(`IAM token exchange failed (${res.status}): ${text}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const data = (await res.json()) as IamTokenResponse;
|
|
114
|
+
return {
|
|
115
|
+
token: data.iamToken,
|
|
116
|
+
expiresAt: new Date(data.expiresAt).getTime(),
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function buildModels(folderId: string) {
|
|
121
|
+
return KNOWN_MODELS.map((m) => ({
|
|
122
|
+
id: `gpt://${folderId}/${m.slug}/latest`,
|
|
123
|
+
name: m.name,
|
|
124
|
+
api: "openai-responses" as const,
|
|
125
|
+
provider: "yandex",
|
|
126
|
+
baseUrl: AI_BASE_URL,
|
|
127
|
+
reasoning: false,
|
|
128
|
+
input: ["text"] as ("text" | "image")[],
|
|
129
|
+
cost: m.cost,
|
|
130
|
+
contextWindow: m.contextWindow,
|
|
131
|
+
maxTokens: m.maxTokens,
|
|
132
|
+
headers: { "OpenAI-Project": folderId },
|
|
133
|
+
compat: {
|
|
134
|
+
supportsDeveloperRole: false,
|
|
135
|
+
supportsReasoningEffort: false,
|
|
136
|
+
maxTokensField: "max_tokens" as const,
|
|
137
|
+
},
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readAuthJson(): Record<string, unknown> {
|
|
142
|
+
try {
|
|
143
|
+
return JSON.parse(readFileSync(AUTH_PATH, "utf8")) as Record<
|
|
144
|
+
string,
|
|
145
|
+
unknown
|
|
146
|
+
>;
|
|
147
|
+
} catch {
|
|
148
|
+
return {};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function writeAuthJson(data: Record<string, unknown>) {
|
|
153
|
+
writeFileSync(AUTH_PATH, JSON.stringify(data, null, 2), "utf8");
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── OAuth local callback server ──────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
const CALLBACK_HTML = `<!DOCTYPE html>
|
|
159
|
+
<html><head><meta charset="utf-8"><title>pi-yandex-bridge</title></head>
|
|
160
|
+
<body style="font-family:sans-serif;padding:2rem">
|
|
161
|
+
<p>Authorizing pi-yandex-bridge…</p>
|
|
162
|
+
<script>
|
|
163
|
+
const p = new URLSearchParams(location.hash.slice(1));
|
|
164
|
+
const t = p.get('access_token');
|
|
165
|
+
if (t) {
|
|
166
|
+
fetch('/token', { method: 'POST', body: t })
|
|
167
|
+
.then(() => { document.body.innerHTML = '<p>✓ Authorized. You can close this tab.</p>'; });
|
|
168
|
+
} else {
|
|
169
|
+
document.body.innerHTML = '<p>Error: no access_token in response.</p>';
|
|
170
|
+
}
|
|
171
|
+
</script>
|
|
172
|
+
</body></html>`;
|
|
173
|
+
|
|
174
|
+
function captureOAuthToken(
|
|
175
|
+
onProgress?: (msg: string) => void,
|
|
176
|
+
): Promise<string> {
|
|
177
|
+
return new Promise((resolve, reject) => {
|
|
178
|
+
const server = createServer((req, res) => {
|
|
179
|
+
if (req.url?.startsWith("/callback")) {
|
|
180
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
181
|
+
res.end(CALLBACK_HTML);
|
|
182
|
+
} else if (req.url === "/token" && req.method === "POST") {
|
|
183
|
+
let body = "";
|
|
184
|
+
req.on("data", (chunk) => (body += chunk));
|
|
185
|
+
req.on("end", () => {
|
|
186
|
+
res.writeHead(200);
|
|
187
|
+
res.end();
|
|
188
|
+
server.close();
|
|
189
|
+
resolve(body.trim());
|
|
190
|
+
});
|
|
191
|
+
} else {
|
|
192
|
+
res.writeHead(404);
|
|
193
|
+
res.end();
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
server.on("error", (err: NodeJS.ErrnoException) => {
|
|
198
|
+
if (err.code === "EADDRINUSE") {
|
|
199
|
+
reject(
|
|
200
|
+
new Error(
|
|
201
|
+
`Port ${OAUTH_CALLBACK_PORT} is already in use. Stop the process using it and try again.`,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
} else {
|
|
205
|
+
reject(new Error(`Local auth server error: ${err.message}`));
|
|
206
|
+
}
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const timeout = setTimeout(
|
|
210
|
+
() => {
|
|
211
|
+
server.close();
|
|
212
|
+
reject(new Error("OAuth authorization timed out after 5 minutes."));
|
|
213
|
+
},
|
|
214
|
+
5 * 60 * 1000,
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
server.listen(OAUTH_CALLBACK_PORT, "127.0.0.1", () => {
|
|
218
|
+
onProgress?.(
|
|
219
|
+
`[yandex] Listening on port ${(server.address() as AddressInfo).port}. Opening browser…`,
|
|
220
|
+
);
|
|
221
|
+
openBrowser(OAUTH_URL);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
server.on("close", () => clearTimeout(timeout));
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ─── OAuth provider callbacks ─────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
async function yandexLogin(
|
|
231
|
+
callbacks: OAuthLoginCallbacks,
|
|
232
|
+
): Promise<OAuthCredentials> {
|
|
233
|
+
let oauthToken = process.env.YANDEX_OAUTH_TOKEN ?? "";
|
|
234
|
+
if (!oauthToken) {
|
|
235
|
+
oauthToken = await captureOAuthToken((msg) => callbacks.onProgress?.(msg));
|
|
236
|
+
} else {
|
|
237
|
+
callbacks.onProgress?.(
|
|
238
|
+
"[yandex] Using YANDEX_OAUTH_TOKEN from environment.",
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
let folderId = process.env.YANDEX_FOLDER_ID ?? "";
|
|
243
|
+
if (!folderId) {
|
|
244
|
+
folderId = await callbacks.onPrompt({
|
|
245
|
+
message: "Enter your Yandex Cloud folder ID:",
|
|
246
|
+
placeholder: "b1g...",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
callbacks.onProgress?.("[yandex] Exchanging OAuth token for IAM token…");
|
|
251
|
+
const iam = await exchangeOAuthForIam(oauthToken);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
refresh: oauthToken,
|
|
255
|
+
access: iam.token,
|
|
256
|
+
expires: iam.expiresAt,
|
|
257
|
+
folderId,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function yandexRefreshToken(
|
|
262
|
+
credentials: OAuthCredentials,
|
|
263
|
+
): Promise<OAuthCredentials> {
|
|
264
|
+
const iam = await exchangeOAuthForIam(credentials.refresh);
|
|
265
|
+
return { ...credentials, access: iam.token, expires: iam.expiresAt };
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── /yalogin command ─────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
async function runYaLogin(ctx: ExtensionCommandContext) {
|
|
271
|
+
try {
|
|
272
|
+
ctx.ui.notify("Opening browser for Yandex authorization…", "info");
|
|
273
|
+
|
|
274
|
+
const oauthToken = await captureOAuthToken();
|
|
275
|
+
|
|
276
|
+
let folderId = process.env.YANDEX_FOLDER_ID ?? "";
|
|
277
|
+
if (!folderId) {
|
|
278
|
+
folderId = (await ctx.ui.input("Yandex Cloud folder ID", "b1g...")) ?? "";
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!folderId) {
|
|
282
|
+
ctx.ui.notify("Login cancelled: folder ID is required.", "error");
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
ctx.ui.notify("Exchanging OAuth token for IAM token…", "info");
|
|
287
|
+
const iam = await exchangeOAuthForIam(oauthToken);
|
|
288
|
+
|
|
289
|
+
const auth = readAuthJson();
|
|
290
|
+
auth.yandex = {
|
|
291
|
+
type: "oauth",
|
|
292
|
+
refresh: oauthToken,
|
|
293
|
+
access: iam.token,
|
|
294
|
+
expires: iam.expiresAt,
|
|
295
|
+
folderId,
|
|
296
|
+
};
|
|
297
|
+
writeAuthJson(auth);
|
|
298
|
+
|
|
299
|
+
ctx.ui.notify(
|
|
300
|
+
"✓ Yandex credentials saved. Restart Pi to activate the models.",
|
|
301
|
+
"info",
|
|
302
|
+
);
|
|
303
|
+
} catch (err) {
|
|
304
|
+
ctx.ui.notify(
|
|
305
|
+
`Yandex login failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
306
|
+
"error",
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// ─── extension entry point ────────────────────────────────────────────────────
|
|
312
|
+
|
|
313
|
+
export default async function (pi: ExtensionAPI) {
|
|
314
|
+
const apiKey = process.env.YANDEX_API_KEY;
|
|
315
|
+
const folderId = process.env.YANDEX_FOLDER_ID;
|
|
316
|
+
|
|
317
|
+
// Static API key path — both env vars must be present.
|
|
318
|
+
if (apiKey && folderId) {
|
|
319
|
+
const modelIds = new Set(
|
|
320
|
+
KNOWN_MODELS.map((m) => `gpt://${folderId}/${m.slug}/latest`),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const res = await fetch(`${AI_BASE_URL}/models`, {
|
|
325
|
+
headers: {
|
|
326
|
+
Authorization: `Bearer ${apiKey}`,
|
|
327
|
+
"OpenAI-Project": folderId,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
if (res.ok) {
|
|
331
|
+
const payload = (await res.json()) as { data: Array<{ id: string }> };
|
|
332
|
+
for (const { id } of payload.data) modelIds.add(id);
|
|
333
|
+
}
|
|
334
|
+
} catch {
|
|
335
|
+
/* static list is enough */
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const models = [...modelIds].map((id) => {
|
|
339
|
+
const slug = id.replace(/^gpt:\/\/[^/]+\//, "").replace(/\/latest$/, "");
|
|
340
|
+
const known = KNOWN_MODELS.find((m) => m.slug === slug);
|
|
341
|
+
return {
|
|
342
|
+
id,
|
|
343
|
+
name: prettyModelName(id),
|
|
344
|
+
reasoning: false,
|
|
345
|
+
input: ["text"] as ("text" | "image")[],
|
|
346
|
+
cost: known?.cost ?? {
|
|
347
|
+
input: 0,
|
|
348
|
+
output: 0,
|
|
349
|
+
cacheRead: 0,
|
|
350
|
+
cacheWrite: 0,
|
|
351
|
+
},
|
|
352
|
+
contextWindow: known?.contextWindow ?? 128_000,
|
|
353
|
+
maxTokens: known?.maxTokens ?? 8_192,
|
|
354
|
+
headers: { "OpenAI-Project": folderId },
|
|
355
|
+
compat: {
|
|
356
|
+
supportsDeveloperRole: false,
|
|
357
|
+
supportsReasoningEffort: false,
|
|
358
|
+
maxTokensField: "max_tokens" as const,
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
pi.registerProvider("yandex", {
|
|
364
|
+
name: "Yandex Cloud",
|
|
365
|
+
baseUrl: AI_BASE_URL,
|
|
366
|
+
apiKey,
|
|
367
|
+
api: "openai-responses",
|
|
368
|
+
models,
|
|
369
|
+
} satisfies ProviderConfig);
|
|
370
|
+
|
|
371
|
+
console.log(
|
|
372
|
+
`[yandex] Registered ${models.length} model(s) with static API key.`,
|
|
373
|
+
);
|
|
374
|
+
} else {
|
|
375
|
+
// OAuth path — seed models from auth.json if folderId is already stored.
|
|
376
|
+
let storedFolderId: string | undefined;
|
|
377
|
+
try {
|
|
378
|
+
const auth = readAuthJson() as Record<string, { folderId?: string }>;
|
|
379
|
+
storedFolderId = auth.yandex?.folderId;
|
|
380
|
+
} catch {
|
|
381
|
+
/* auth.json absent or unreadable */
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
pi.registerProvider("yandex", {
|
|
385
|
+
name: "Yandex Cloud",
|
|
386
|
+
baseUrl: AI_BASE_URL,
|
|
387
|
+
api: "openai-responses",
|
|
388
|
+
models: storedFolderId ? buildModels(storedFolderId) : [],
|
|
389
|
+
oauth: {
|
|
390
|
+
name: "Yandex Cloud (OAuth)",
|
|
391
|
+
login: yandexLogin,
|
|
392
|
+
refreshToken: yandexRefreshToken,
|
|
393
|
+
getApiKey: (credentials) => credentials.access,
|
|
394
|
+
modifyModels: (_, credentials) =>
|
|
395
|
+
buildModels(credentials.folderId as string),
|
|
396
|
+
},
|
|
397
|
+
} satisfies ProviderConfig);
|
|
398
|
+
|
|
399
|
+
console.log("[yandex] Registered with OAuth.");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
pi.registerCommand("yalogin", {
|
|
403
|
+
description: "Authorize Yandex Cloud (opens browser, no pasting required)",
|
|
404
|
+
handler: async (_args, ctx) => runYaLogin(ctx),
|
|
405
|
+
});
|
|
406
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pi-yandex-bridge",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pi Coding Agent provider bridge for Yandex Cloud AI (YandexGPT)",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"pi": {
|
|
8
|
+
"extensions": ["./dist/index.js"],
|
|
9
|
+
"image": "https://upload.wikimedia.org/wikipedia/commons/5/5b/Yandex_cloud_logo_new.jpg"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsc"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"pi-package",
|
|
16
|
+
"pi",
|
|
17
|
+
"pi-coding-agent",
|
|
18
|
+
"yandex",
|
|
19
|
+
"yandexgpt"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"@earendil-works/pi-coding-agent": "*"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {
|
|
28
|
+
"@earendil-works/pi-coding-agent": "latest",
|
|
29
|
+
"typescript": "^5"
|
|
30
|
+
},
|
|
31
|
+
"license": "MIT"
|
|
32
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": ".",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["index.ts"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|