@toddzheng024/dscode-desktop 0.7.42 → 0.7.44
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 +2 -2
- package/cordis.patch.yml +11 -2
- package/docs/CHANGELOG.md +31 -0
- package/docs/antigravity.md +41 -0
- package/docs/distribution.md +3 -3
- package/docs/dscode-ultra.md +16 -3
- package/docs/openai.md +64 -0
- package/docs/releases/0.7.42.md +1 -1
- package/docs/releases/0.7.43.md +41 -0
- package/docs/releases/0.7.44.md +43 -0
- package/docs/tui-commands.md +13 -8
- package/package.json +4 -1
- package/plugins/antigravity/NOTICE +25 -0
- package/plugins/antigravity/adapter.mjs +79 -0
- package/plugins/antigravity/client.mjs +73 -0
- package/plugins/antigravity/command.mjs +79 -0
- package/plugins/antigravity/index.mjs +49 -0
- package/plugins/antigravity/models.mjs +80 -0
- package/plugins/antigravity/oauth.mjs +130 -0
- package/plugins/antigravity/wire.mjs +155 -0
- package/plugins/compaction/engine.mjs +4 -3
- package/plugins/compaction/threshold.mjs +10 -2
- package/plugins/credentials/index.mjs +7 -0
- package/plugins/dscode/index.mjs +3 -2
- package/plugins/openai/adapter.mjs +52 -0
- package/plugins/openai/auth.mjs +81 -0
- package/plugins/openai/command.mjs +66 -0
- package/plugins/openai/http.mjs +29 -0
- package/plugins/openai/index.mjs +58 -0
- package/plugins/openai/models.mjs +59 -0
- package/plugins/openai/oauth.mjs +82 -0
- package/plugins/openai/speed.d.mts +2 -0
- package/plugins/openai/speed.mjs +48 -0
- package/plugins/openai/store.mjs +44 -0
- package/plugins/openai/wire.mjs +93 -0
- package/plugins/providers/catalog.mjs +2 -0
- package/plugins/providers/startup-commands.d.mts +6 -0
- package/plugins/providers/startup-commands.mjs +27 -0
- package/vendor/persistent/index.js +3 -20
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { LlmAdapter, LlmError, ReasoningEffortId } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { isAttachmentError } from '@deepseek-ai/dsh-attachment';
|
|
3
|
+
import { prepareImages } from '../custom/images.mjs';
|
|
4
|
+
import { sseData } from '../providers/chat-stream.mjs';
|
|
5
|
+
import { headers, responseError } from './client.mjs';
|
|
6
|
+
import { modelRoute } from './models.mjs';
|
|
7
|
+
import { requestBody, translate } from './wire.mjs';
|
|
8
|
+
|
|
9
|
+
/** Cancelling one caller must not cancel a shared token refresh/catalog read. */
|
|
10
|
+
async function abortable(promise, signal) {
|
|
11
|
+
signal.throwIfAborted();
|
|
12
|
+
let abort;
|
|
13
|
+
try {
|
|
14
|
+
return await Promise.race([promise, new Promise((_, reject) => {
|
|
15
|
+
abort = () => reject(signal.reason); signal.addEventListener('abort', abort, { once: true });
|
|
16
|
+
})]);
|
|
17
|
+
} finally { signal.removeEventListener('abort', abort); }
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class AntigravityAdapter extends LlmAdapter {
|
|
21
|
+
constructor(config) { super(); this.config = config; }
|
|
22
|
+
providerInfo(provider) { return { id: provider, name: 'Antigravity' }; }
|
|
23
|
+
providerRetryPolicy() { return this.config.options().retryPolicy; }
|
|
24
|
+
async listModels(provider) {
|
|
25
|
+
return (await this.config.models.list()).map(m => ({ provider, id: m.id, name: m.name, inputModalities: m.inputModalities ?? ['text', 'image'] }));
|
|
26
|
+
}
|
|
27
|
+
async resolveModel(provider, id) {
|
|
28
|
+
const m = await this.config.models.get(id);
|
|
29
|
+
const efforts = Object.keys(m.routes).filter(e => e !== 'default');
|
|
30
|
+
return { provider, id, name: m.name, inputModalities: m.inputModalities ?? ['text', 'image'], context: { contextWindow: m.contextWindow }, defaultMaxTokens: m.maxTokens,
|
|
31
|
+
...(efforts.length ? { reasoning: { efforts: efforts.map(id => ({ id: ReasoningEffortId(id), name: id })), defaultEffort: ReasoningEffortId(efforts.includes('low') ? 'low' : efforts[0]) } } : {}) };
|
|
32
|
+
}
|
|
33
|
+
async *stream(options) {
|
|
34
|
+
const { session, models, client } = this.config;
|
|
35
|
+
const config = this.config.options();
|
|
36
|
+
const controller = new AbortController();
|
|
37
|
+
const signal = AbortSignal.any([controller.signal, ...(options.signal ? [options.signal] : [])]);
|
|
38
|
+
let timer, timedOut = false;
|
|
39
|
+
const pulse = ms => {
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
timer = setTimeout(() => { timedOut = true; controller.abort(); }, ms);
|
|
42
|
+
timer.unref?.();
|
|
43
|
+
};
|
|
44
|
+
try {
|
|
45
|
+
signal.throwIfAborted();
|
|
46
|
+
const grant = await abortable(session.auth(), signal);
|
|
47
|
+
const model = await abortable(models.get(options.model), signal), route = modelRoute(model, options.reasoningEffort);
|
|
48
|
+
const attachments = this.config.resolveAttachments?.();
|
|
49
|
+
const images = await prepareImages(options.messages, { ...model, inputModalities: model.inputModalities ?? ['text', 'image'] }, attachments,
|
|
50
|
+
ref => attachments && this.config.resolveImageAccess?.(attachments, ref), signal);
|
|
51
|
+
signal.throwIfAborted();
|
|
52
|
+
const body = JSON.stringify(requestBody({ ...options, messages: images.messages }, model, route, grant.projectId, images));
|
|
53
|
+
let response;
|
|
54
|
+
pulse(config.headerTimeoutMs);
|
|
55
|
+
for (const endpoint of client.endpoints) {
|
|
56
|
+
response = await client.fetch(`${endpoint}/v1internal:streamGenerateContent?alt=sse`, {
|
|
57
|
+
method: 'POST', redirect: 'error', headers: { ...headers(grant.access), accept: 'text/event-stream' }, body, signal,
|
|
58
|
+
});
|
|
59
|
+
if (response.ok) break;
|
|
60
|
+
const error = await responseError(response);
|
|
61
|
+
// Only a missing endpoint is retried here; rate limits and transient
|
|
62
|
+
// failures belong to the Harness retry policy, never nested loops.
|
|
63
|
+
if (response.status !== 404 || endpoint === client.endpoints.at(-1)) throw error;
|
|
64
|
+
}
|
|
65
|
+
if (!response?.body || !response.headers.get('content-type')?.includes('text/event-stream')) throw new LlmError('Antigravity did not return a stream', 'MALFORMED_RESPONSE');
|
|
66
|
+
pulse(config.streamIdleTimeoutMs);
|
|
67
|
+
for await (const chunk of translate(sseData(response.body, () => pulse(config.streamIdleTimeoutMs)), { model: model.id, runtimeModel: route.id })) {
|
|
68
|
+
signal.throwIfAborted();
|
|
69
|
+
clearTimeout(timer); yield chunk; pulse(config.streamIdleTimeoutMs);
|
|
70
|
+
}
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (options.signal?.aborted) throw new LlmError('Antigravity request cancelled', 'ABORTED');
|
|
73
|
+
if (timedOut) throw new LlmError('Antigravity connection timed out; retry the request', 'TIMEOUT');
|
|
74
|
+
if (isAttachmentError(error)) throw new LlmError(`Antigravity image input failed: ${error.code}`, error.code);
|
|
75
|
+
if (error instanceof LlmError) throw error;
|
|
76
|
+
throw new LlmError('Could not complete the Antigravity request', 'TRANSPORT');
|
|
77
|
+
} finally { clearTimeout(timer); controller.abort(); }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { errorCode, retryAfterMs } from '../providers/http-errors.mjs';
|
|
3
|
+
|
|
4
|
+
// Wire compatibility follows pi-antigravity d59d22b (see NOTICE). This is an
|
|
5
|
+
// unofficial model transport, independent of both the ACP agent and the CLI.
|
|
6
|
+
export const ENDPOINTS = Object.freeze([
|
|
7
|
+
'https://daily-cloudcode-pa.googleapis.com',
|
|
8
|
+
'https://daily-cloudcode-pa.sandbox.googleapis.com',
|
|
9
|
+
'https://cloudcode-pa.googleapis.com',
|
|
10
|
+
]);
|
|
11
|
+
export const USER_AGENT = 'antigravity/cli/1.2.4 (aidev_client; os_type=linux; arch=amd64; cl=982146307; auth_method=consumer)';
|
|
12
|
+
export const headers = token => ({ authorization: `Bearer ${token}`, 'content-type': 'application/json', 'User-Agent': USER_AGENT });
|
|
13
|
+
|
|
14
|
+
export function projectId(data) {
|
|
15
|
+
if (!data || typeof data !== 'object') return undefined;
|
|
16
|
+
for (const key of ['antigravityProjectId', 'projectId', 'backendProjectId', 'userDefinedCloudaicompanionProject', 'cloudaicompanionProject', 'project']) {
|
|
17
|
+
const value = data[key], id = typeof value === 'string' ? value : value?.id;
|
|
18
|
+
if (typeof id === 'string' && id.trim()) return id;
|
|
19
|
+
}
|
|
20
|
+
for (const key of ['projects', 'projectIds', 'cloudaicompanionProjects']) {
|
|
21
|
+
for (const item of Array.isArray(data[key]) ? data[key] : []) {
|
|
22
|
+
const id = typeof item === 'string' ? item : projectId(item);
|
|
23
|
+
if (id) return id;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Never put server text, URLs containing auth codes, or fetch causes into logs. */
|
|
30
|
+
export async function responseError(response) {
|
|
31
|
+
let error;
|
|
32
|
+
try { error = (await response.json())?.error; } catch { /* status is sufficient */ }
|
|
33
|
+
const code = errorCode(response.status, typeof error === 'object' ? error : undefined);
|
|
34
|
+
const delay = retryAfterMs(response.headers.get('retry-after'));
|
|
35
|
+
return new LlmError(`Antigravity request failed (HTTP ${response.status})`, code,
|
|
36
|
+
{ status: response.status, ...(delay === undefined ? {} : { providerRetryAfterMs: delay }) });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class AntigravityClient {
|
|
40
|
+
constructor({ fetch = globalThis.fetch, endpoints = ENDPOINTS, metadataTimeoutMs = 10000 } = {}) {
|
|
41
|
+
this.fetch = fetch; this.endpoints = endpoints; this.metadataTimeoutMs = metadataTimeoutMs;
|
|
42
|
+
}
|
|
43
|
+
async json(method, token, body, signal) {
|
|
44
|
+
const deadline = AbortSignal.timeout(this.metadataTimeoutMs);
|
|
45
|
+
const combined = AbortSignal.any([deadline, ...(signal ? [signal] : [])]);
|
|
46
|
+
let last;
|
|
47
|
+
for (const endpoint of this.endpoints) {
|
|
48
|
+
try {
|
|
49
|
+
const response = await this.fetch(`${endpoint}/v1internal:${method}`, {
|
|
50
|
+
method: 'POST', redirect: 'error', headers: headers(token), body: JSON.stringify(body), signal: combined,
|
|
51
|
+
});
|
|
52
|
+
if (response.ok) return await response.json();
|
|
53
|
+
last = await responseError(response);
|
|
54
|
+
// A quota/auth rejection is final; changing endpoints must not hide it.
|
|
55
|
+
if (![404, 500, 502, 503, 504].includes(response.status)) throw last;
|
|
56
|
+
} catch (error) {
|
|
57
|
+
if (signal?.aborted) throw new LlmError('Antigravity request cancelled', 'ABORTED');
|
|
58
|
+
if (deadline.aborted) throw new LlmError('Antigravity discovery timed out', 'TIMEOUT');
|
|
59
|
+
if (error instanceof LlmError) throw error;
|
|
60
|
+
last = new LlmError('Could not connect to Antigravity', 'TRANSPORT');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
throw last ?? new LlmError('No Antigravity endpoint available', 'TRANSPORT');
|
|
64
|
+
}
|
|
65
|
+
async project(token, signal) {
|
|
66
|
+
const data = await this.json('loadCodeAssist', token, { metadata: { ideType: 'ANTIGRAVITY' } }, signal);
|
|
67
|
+
let id = projectId(data);
|
|
68
|
+
if (!id) id = projectId(await this.json('listCloudAICompanionProjects', token, {}, signal));
|
|
69
|
+
if (typeof id !== 'string' || !id) throw new LlmError('Antigravity did not return an enabled project for this account; open Antigravity to finish account setup, then sign in again', 'AUTH');
|
|
70
|
+
return id;
|
|
71
|
+
}
|
|
72
|
+
models(grant, signal) { return this.json('fetchAvailableModels', grant.access, { project: grant.projectId }, signal); }
|
|
73
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { openBrowser } from '../account/index.mjs';
|
|
2
|
+
import { authorize, callbackServer, exchangeCode } from './oauth.mjs';
|
|
3
|
+
|
|
4
|
+
const message = (locale, en, zh) => locale.startsWith('zh') ? zh : en;
|
|
5
|
+
|
|
6
|
+
export class AntigravityLogin {
|
|
7
|
+
constructor({ session, client, models, open = openBrowser, callback = callbackServer, exchange = exchangeCode }) {
|
|
8
|
+
Object.assign(this, { session, client, models, open, callback, exchange });
|
|
9
|
+
}
|
|
10
|
+
async status(locale) {
|
|
11
|
+
const grant = await this.session.current();
|
|
12
|
+
return [this.attempt ? message(locale, 'Waiting for Google sign-in in your browser. /antigravity cancel stops it.', '等待浏览器中的 Google 登录。/antigravity cancel 可取消。') : this.outcome,
|
|
13
|
+
grant ? message(locale, 'Antigravity is signed in. Use /provider antigravity. Tokens refresh automatically.', 'Antigravity 已登录。使用 /provider antigravity 切换;凭据会自动续期。')
|
|
14
|
+
: message(locale, 'Antigravity is not signed in. Run /antigravity login.', 'Antigravity 未登录。运行 /antigravity login。')].filter(Boolean).join('\n');
|
|
15
|
+
}
|
|
16
|
+
async login(locale) {
|
|
17
|
+
if (this.attempt) return this.status(locale);
|
|
18
|
+
const attempt = { ...authorize(), controller: new AbortController(), revision: this.session.revision };
|
|
19
|
+
this.attempt = attempt; this.outcome = undefined;
|
|
20
|
+
try {
|
|
21
|
+
attempt.listener = await this.callback(attempt.state, { signal: attempt.controller.signal });
|
|
22
|
+
if (this.attempt !== attempt) { attempt.listener.close(); return this.status(locale); }
|
|
23
|
+
attempt.done = this.follow(attempt, locale);
|
|
24
|
+
await this.open(attempt.url).catch(() => false);
|
|
25
|
+
return message(locale,
|
|
26
|
+
'Complete Google sign-in for Antigravity (community integration). Waiting for up to 5 minutes; /antigravity shows the result. If the browser did not open:',
|
|
27
|
+
'请在浏览器中完成 Antigravity 的 Google 登录(社区接入)。最多等待 5 分钟;/antigravity 查看结果。如果浏览器未打开,请访问:') + '\n' + attempt.url;
|
|
28
|
+
} catch {
|
|
29
|
+
if (this.attempt === attempt) this.attempt = undefined;
|
|
30
|
+
throw new Error(message(locale, 'Could not open the Antigravity login callback on port 51121. Close another pending login and retry.', '无法在 51121 端口启动登录回调。请关闭其他正在等待的登录后重试。'));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async follow(attempt, locale) {
|
|
34
|
+
try {
|
|
35
|
+
const code = await attempt.listener.code;
|
|
36
|
+
const grant = await this.exchange(code, attempt.verifier, this.client, { fetch: this.client.fetch, signal: attempt.controller.signal });
|
|
37
|
+
if (this.attempt !== attempt || attempt.controller.signal.aborted) return;
|
|
38
|
+
if (await this.session.save(grant, attempt.revision)) {
|
|
39
|
+
this.models.clear();
|
|
40
|
+
this.outcome = message(locale, 'Google sign-in completed. Use /provider antigravity.', 'Google 登录完成。使用 /provider antigravity。');
|
|
41
|
+
}
|
|
42
|
+
} catch {
|
|
43
|
+
if (this.attempt === attempt) this.outcome = message(locale, 'Antigravity sign-in did not complete. Check account setup and retry /antigravity login.', 'Antigravity 登录未完成。请检查账号设置后重试 /antigravity login。');
|
|
44
|
+
} finally {
|
|
45
|
+
attempt.listener.close();
|
|
46
|
+
if (this.attempt === attempt) this.attempt = undefined;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
cancel(locale) {
|
|
50
|
+
this.session.revision++;
|
|
51
|
+
this.attempt?.controller.abort(); this.attempt?.listener?.close(); this.attempt = undefined;
|
|
52
|
+
return this.outcome = message(locale, 'Antigravity sign-in cancelled.', '已取消 Antigravity 登录。');
|
|
53
|
+
}
|
|
54
|
+
async logout(locale) {
|
|
55
|
+
this.cancel(locale); await this.session.logout(); this.models.clear(); this.outcome = undefined;
|
|
56
|
+
return message(locale, 'Removed the DSCODE Antigravity login. Other applications are unchanged.', '已移除 DSCODE 的 Antigravity 登录,其他应用的登录不变。');
|
|
57
|
+
}
|
|
58
|
+
dispose() { this.cancel('en'); }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function antigravityCommand(login, language) {
|
|
62
|
+
return { name: 'antigravity', description: 'Antigravity Google login and Gemini models', input: { hint: '[login|status|models|cancel|logout]' },
|
|
63
|
+
handler: async ({ rawInput }) => {
|
|
64
|
+
const locale = language(), arg = String(rawInput ?? '').trim().toLowerCase();
|
|
65
|
+
try {
|
|
66
|
+
let text;
|
|
67
|
+
if (!arg || arg === 'status') text = await login.status(locale);
|
|
68
|
+
else if (arg === 'login') text = await login.login(locale);
|
|
69
|
+
else if (arg === 'cancel') text = login.cancel(locale);
|
|
70
|
+
else if (arg === 'logout') text = await login.logout(locale);
|
|
71
|
+
else if (arg === 'models') {
|
|
72
|
+
const models = await login.models.list(true);
|
|
73
|
+
text = [login.models.discovered ? 'Antigravity account model catalog' : 'Antigravity fallback catalog (availability not verified)', login.models.error,
|
|
74
|
+
...models.map(m => `${m.id} · ${Object.keys(m.routes).join('/')} · ${m.contextWindow} context`)].filter(Boolean).join('\n');
|
|
75
|
+
} else return { kind: 'error', text: 'Usage: /antigravity [login|status|models|cancel|logout]' };
|
|
76
|
+
return { kind: 'success', text };
|
|
77
|
+
} catch { return { kind: 'error', text: message(locale, 'Antigravity operation failed. Check port 51121 and account connectivity, then retry.', 'Antigravity 操作失败。请检查 51121 端口和账号连接后重试。') }; }
|
|
78
|
+
} };
|
|
79
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { registerStartupCommand } from '../providers/startup-commands.mjs';
|
|
2
|
+
import z from '@deepseek-ai/schemastery';
|
|
3
|
+
import { RetryPolicySchema, resolveRetryPolicy, resolveImageAttachmentAccess } from '@deepseek-ai/dsh-llm';
|
|
4
|
+
import { plainConfig } from '../cordis-config/plain.mjs';
|
|
5
|
+
import { readLanguage } from '../i18n/messages.mjs';
|
|
6
|
+
import { AntigravityClient } from './client.mjs';
|
|
7
|
+
import { AntigravitySession, GRANT_REF } from './oauth.mjs';
|
|
8
|
+
import { AntigravityModels, PROVIDER } from './models.mjs';
|
|
9
|
+
import { AntigravityAdapter } from './adapter.mjs';
|
|
10
|
+
import { AntigravityLogin, antigravityCommand } from './command.mjs';
|
|
11
|
+
|
|
12
|
+
export const name = 'dscode-antigravity';
|
|
13
|
+
export const inject = ['llm', 'credentials'];
|
|
14
|
+
export const Config = z.object({
|
|
15
|
+
apiKeyEnv: z.union([GRANT_REF]).role('credential-ref').default(GRANT_REF).volatile(),
|
|
16
|
+
headerTimeoutMs: z.number().min(1).default(30000).volatile(),
|
|
17
|
+
streamIdleTimeoutMs: z.number().min(1).default(120000).volatile(),
|
|
18
|
+
retryPolicy: RetryPolicySchema.volatile(),
|
|
19
|
+
});
|
|
20
|
+
export function resolveOptions(config = {}) {
|
|
21
|
+
const headerTimeoutMs = config.headerTimeoutMs ?? 30000, streamIdleTimeoutMs = config.streamIdleTimeoutMs ?? 120000;
|
|
22
|
+
if (![headerTimeoutMs, streamIdleTimeoutMs].every(n => Number.isFinite(n) && n > 0)) throw new Error('Antigravity timeouts must be positive');
|
|
23
|
+
return { headerTimeoutMs, streamIdleTimeoutMs, retryPolicy: resolveRetryPolicy(config.retryPolicy, name) };
|
|
24
|
+
}
|
|
25
|
+
export function apply(ctx, config = {}) {
|
|
26
|
+
ctx.inject(['settings'], scoped => scoped.effect(() => scoped.settings.configure({ auto: false }, ctx.fiber)));
|
|
27
|
+
const session = new AntigravitySession({
|
|
28
|
+
read: async () => (await ctx.credentials.resolve(GRANT_REF))?.value,
|
|
29
|
+
write: value => ctx.credentials.set(GRANT_REF, value), remove: () => ctx.credentials.unset(GRANT_REF),
|
|
30
|
+
});
|
|
31
|
+
const client = new AntigravityClient(), models = new AntigravityModels(session, client);
|
|
32
|
+
let last = resolveOptions(plainConfig(config));
|
|
33
|
+
const options = () => { try { last = resolveOptions(plainConfig(config)); } catch { /* retain last valid options */ } return last; };
|
|
34
|
+
const adapter = new AntigravityAdapter({ session, client, models, options,
|
|
35
|
+
resolveAttachments: () => ctx.get('attachments'),
|
|
36
|
+
resolveImageAccess: (attachments, ref) => resolveImageAttachmentAccess(attachments, path => ctx.get('fs')?.processPathFromHostPath(path), ref),
|
|
37
|
+
});
|
|
38
|
+
const login = new AntigravityLogin({ session, client, models });
|
|
39
|
+
ctx.provide('dscodeAntigravityLogin', login);
|
|
40
|
+
ctx.effect(() => () => login.dispose());
|
|
41
|
+
ctx.inject(['commands'], scoped => registerStartupCommand(scoped, antigravityCommand(login, () => readLanguage())));
|
|
42
|
+
ctx.llm.registerConfigurableProviders([{ provider: PROVIDER, displayName: 'Antigravity', settingsNs: ctx.fiber.entry?.options.id ?? name, settingsPath: [] }]);
|
|
43
|
+
const registration = ctx.llm.registerAdapter([PROVIDER], adapter);
|
|
44
|
+
let policy = JSON.stringify(options().retryPolicy);
|
|
45
|
+
ctx.on('loader/volatile-update', () => {
|
|
46
|
+
const next = JSON.stringify(options().retryPolicy);
|
|
47
|
+
if (next !== policy) { registration.replace([PROVIDER]); policy = next; }
|
|
48
|
+
});
|
|
49
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
|
|
3
|
+
export const PROVIDER = 'antigravity';
|
|
4
|
+
const positive = n => Number.isSafeInteger(n) && n > 0;
|
|
5
|
+
// Conservative fallback catalog from pi-antigravity d59d22b. Runtime discovery
|
|
6
|
+
// narrows these routes to the variants actually advertised by the account.
|
|
7
|
+
const flash = (version, enums) => ({ id: `gemini-${version}-flash`, name: `Gemini ${version} Flash`, contextWindow: 1048576, maxTokens: 65536,
|
|
8
|
+
routes: Object.fromEntries(['low', 'medium', 'high'].map((effort, i) => [effort, { id: `gemini-${version}-flash-${effort}`, modelEnum: `MODEL_PLACEHOLDER_M${enums[i]}` }])) });
|
|
9
|
+
export const FALLBACK_MODELS = [flash('3.8', [320, 319, 318]), flash('3.7', [300, 299, 298]), flash('3.6', [73, 72, 71]),
|
|
10
|
+
{ id: 'gemini-3.5-flash', name: 'Gemini 3.5 Flash', contextWindow: 1048576, maxTokens: 65536,
|
|
11
|
+
routes: { low: { id: 'gemini-3.5-flash-extra-low', modelEnum: 'MODEL_PLACEHOLDER_M187' }, medium: { id: 'gemini-3.5-flash-low', modelEnum: 'MODEL_PLACEHOLDER_M20' }, high: { id: 'gemini-3-flash-agent', modelEnum: 'MODEL_PLACEHOLDER_M84' } } },
|
|
12
|
+
{ id: 'gemini-3.1-pro', name: 'Gemini 3.1 Pro', contextWindow: 1048576, maxTokens: 65535,
|
|
13
|
+
routes: { low: { id: 'gemini-3.1-pro-low', modelEnum: 'MODEL_PLACEHOLDER_M36' }, high: { id: 'gemini-pro-agent', modelEnum: 'MODEL_PLACEHOLDER_M16' } } },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function parseModels(data) {
|
|
17
|
+
if (!data?.models || typeof data.models !== 'object' || Array.isArray(data.models)) throw new LlmError('Antigravity returned an invalid model catalog', 'MALFORMED_RESPONSE');
|
|
18
|
+
const models = new Map();
|
|
19
|
+
for (const [runtime, info] of Object.entries(data.models)) {
|
|
20
|
+
if (!/^gemini-[a-z0-9.-]+$/.test(runtime) || /image|embedding|audio|tts|live|tab|chat|tiered/i.test(runtime) || runtime === 'gemini-3.1-pro-high' || !info || typeof info !== 'object') continue;
|
|
21
|
+
const known = FALLBACK_MODELS.find(m => Object.values(m.routes).some(r => r.id === runtime));
|
|
22
|
+
const suffix = runtime.match(/-(low|medium|high)$/)?.[1];
|
|
23
|
+
const effort = known ? Object.entries(known.routes).find(([, r]) => r.id === runtime)[0] : suffix;
|
|
24
|
+
const id = known?.id ?? (suffix ? runtime.slice(0, -suffix.length - 1) : runtime);
|
|
25
|
+
const entry = models.get(id) ?? { id, name: known?.name ?? id, contextWindow: known?.contextWindow ?? 128000, maxTokens: known?.maxTokens ?? 8192, inputModalities: ['text', 'image'], routes: {} };
|
|
26
|
+
if (info.supportsImages === false) entry.inputModalities = ['text'];
|
|
27
|
+
if (positive(info.inputTokenLimit)) entry.contextWindow = info.inputTokenLimit;
|
|
28
|
+
if (positive(info.maxOutputTokens)) entry.maxTokens = info.maxOutputTokens;
|
|
29
|
+
else if (positive(info.outputTokenLimit)) entry.maxTokens = info.outputTokenLimit;
|
|
30
|
+
entry.routes[effort ?? 'default'] = { id: runtime,
|
|
31
|
+
...(typeof info.model === 'string' ? { modelEnum: info.model } : {}),
|
|
32
|
+
...(info.quotaInfo && typeof info.quotaInfo === 'object' ? { quota: info.quotaInfo } : {}) };
|
|
33
|
+
models.set(id, entry);
|
|
34
|
+
}
|
|
35
|
+
return [...models.values()];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export class AntigravityModels {
|
|
39
|
+
constructor(session, client) { this.session = session; this.client = client; this.clear(); }
|
|
40
|
+
clear() { this.entries = FALLBACK_MODELS; this.key = undefined; this.checked = 0; this.generation = (this.generation ?? 0) + 1; this.pending = undefined; this.error = undefined; this.discovered = false; }
|
|
41
|
+
async list(force = false) {
|
|
42
|
+
const grant = await this.session.current();
|
|
43
|
+
if (!grant) { if (this.key) this.clear(); return this.entries; }
|
|
44
|
+
const key = grant.projectId + ':' + grant.refresh;
|
|
45
|
+
if (this.key !== key) { this.clear(); this.key = key; }
|
|
46
|
+
if (!force && Date.now() - this.checked < 300000) return this.entries;
|
|
47
|
+
if (this.pending) return this.pending;
|
|
48
|
+
const generation = this.generation;
|
|
49
|
+
this.pending = (async () => {
|
|
50
|
+
try {
|
|
51
|
+
const models = parseModels(await this.client.models(await this.session.auth()));
|
|
52
|
+
if (generation === this.generation) { this.entries = models; this.discovered = true; this.error = undefined; }
|
|
53
|
+
} catch (error) {
|
|
54
|
+
if (generation === this.generation) this.error = error instanceof LlmError ? error.message : 'Antigravity model discovery failed';
|
|
55
|
+
} finally {
|
|
56
|
+
if (generation === this.generation) { this.checked = Date.now(); this.pending = undefined; }
|
|
57
|
+
}
|
|
58
|
+
return this.entries;
|
|
59
|
+
})();
|
|
60
|
+
return this.pending;
|
|
61
|
+
}
|
|
62
|
+
async get(id) {
|
|
63
|
+
const model = (await this.list()).find(m => m.id === id);
|
|
64
|
+
if (!model) throw new LlmError('This Gemini model is not in the Antigravity catalog; refresh with /antigravity models', 'MODEL_NOT_FOUND');
|
|
65
|
+
return model;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function modelRoute(model, effort) {
|
|
70
|
+
const chosen = effort ?? (model.routes.low ? 'low' : Object.keys(model.routes)[0]);
|
|
71
|
+
const route = model.routes[chosen];
|
|
72
|
+
if (!route) throw new LlmError('This Antigravity model does not offer that reasoning effort', 'UNSUPPORTED_REASONING_EFFORT');
|
|
73
|
+
return { ...route, effort: chosen };
|
|
74
|
+
}
|
|
75
|
+
export function thinkingConfig(route) {
|
|
76
|
+
if (route.effort === 'default') return undefined;
|
|
77
|
+
const pro = route.id === 'gemini-pro-agent' || route.id.startsWith('gemini-3.1-pro');
|
|
78
|
+
const olderFlash = route.id.startsWith('gemini-3.5-flash') || route.id === 'gemini-3-flash-agent';
|
|
79
|
+
return { includeThoughts: true, thinkingBudget: pro ? (route.effort === 'high' ? 10001 : 1001) : route.effort === 'high' ? (olderFlash ? 10000 : -1) : route.effort === 'medium' ? 4000 : 1000 };
|
|
80
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import { createServer } from 'node:http';
|
|
3
|
+
import { LlmError } from '@deepseek-ai/dsh-llm';
|
|
4
|
+
|
|
5
|
+
export const GRANT_REF = 'ANTIGRAVITY_OAUTH';
|
|
6
|
+
export const REDIRECT_URI = 'http://localhost:51121/oauth-callback';
|
|
7
|
+
export const AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
|
8
|
+
export const TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
|
9
|
+
// Public installed-app client distributed by pi-antigravity, not a user secret.
|
|
10
|
+
const CLIENT_ID = '1071006060591-tmhssin2h21lcre235vtolojh4g403ep.apps.googleusercontent.com';
|
|
11
|
+
const CLIENT_SECRET = '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';
|
|
12
|
+
const SCOPES = ['aicode', 'cloud-platform', 'userinfo.email', 'userinfo.profile', 'cclog', 'experimentsandconfigs'].map(scope => `https://www.googleapis.com/auth/${scope}`);
|
|
13
|
+
const MARGIN = 5 * 60 * 1000;
|
|
14
|
+
|
|
15
|
+
export function authorize() {
|
|
16
|
+
const verifier = randomBytes(32).toString('base64url'), state = randomBytes(32).toString('base64url');
|
|
17
|
+
const query = new URLSearchParams({ client_id: CLIENT_ID, redirect_uri: REDIRECT_URI, response_type: 'code',
|
|
18
|
+
scope: SCOPES.join(' '), access_type: 'offline', prompt: 'consent', state,
|
|
19
|
+
code_challenge: createHash('sha256').update(verifier).digest('base64url'), code_challenge_method: 'S256' });
|
|
20
|
+
return { verifier, state, url: `${AUTH_URL}?${query}` };
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A loopback-only callback; invalid state requests never consume the pending login. */
|
|
24
|
+
export async function callbackServer(state, { port = 51121, timeoutMs = MARGIN, signal } = {}) {
|
|
25
|
+
let resolveCode, rejectCode, timer, settled = false;
|
|
26
|
+
const code = new Promise((resolve, reject) => { resolveCode = resolve; rejectCode = reject; });
|
|
27
|
+
// The browser can finish before the caller attaches its continuation.
|
|
28
|
+
code.catch(() => {});
|
|
29
|
+
const finish = (error, value) => {
|
|
30
|
+
if (settled) return;
|
|
31
|
+
settled = true; clearTimeout(timer); signal?.removeEventListener('abort', abort);
|
|
32
|
+
server.close(); server.closeIdleConnections?.();
|
|
33
|
+
if (error) rejectCode(error); else resolveCode(value);
|
|
34
|
+
};
|
|
35
|
+
const abort = () => finish(new Error('Antigravity login cancelled'));
|
|
36
|
+
const server = createServer((req, res) => {
|
|
37
|
+
res.setHeader('content-type', 'text/plain; charset=utf-8');
|
|
38
|
+
res.setHeader('connection', 'close');
|
|
39
|
+
res.setHeader('cache-control', 'no-store'); res.setHeader('referrer-policy', 'no-referrer');
|
|
40
|
+
res.setHeader('content-security-policy', "default-src 'none'");
|
|
41
|
+
let url;
|
|
42
|
+
try { url = new URL(req.url, REDIRECT_URI); } catch { res.writeHead(400); res.end('Invalid callback URL'); return; }
|
|
43
|
+
if (req.method !== 'GET' || url.pathname !== '/oauth-callback') { res.writeHead(404); res.end('Not found'); return; }
|
|
44
|
+
if (url.searchParams.get('state') !== state) { res.writeHead(400); res.end('Invalid login state'); return; }
|
|
45
|
+
const value = url.searchParams.get('code');
|
|
46
|
+
if (url.searchParams.has('error') || !value) {
|
|
47
|
+
res.writeHead(400); res.end('Sign-in was not completed. Return to DSCODE.');
|
|
48
|
+
finish(new Error('Google sign-in was declined or returned no code')); return;
|
|
49
|
+
}
|
|
50
|
+
res.end('Google authorization received. Return to DSCODE and run /antigravity to check completion.');
|
|
51
|
+
finish(undefined, value);
|
|
52
|
+
});
|
|
53
|
+
server.requestTimeout = 5000; server.headersTimeout = 5000;
|
|
54
|
+
try {
|
|
55
|
+
signal?.throwIfAborted();
|
|
56
|
+
await new Promise((resolve, reject) => { server.once('error', reject); server.listen(port, '127.0.0.1', resolve); });
|
|
57
|
+
} catch { server.close(); throw new Error('Cannot open the Antigravity login callback on 127.0.0.1:51121; close another pending login and retry'); }
|
|
58
|
+
server.on('error', () => finish(new Error('Antigravity callback failed')));
|
|
59
|
+
timer = setTimeout(() => finish(new Error('Antigravity login expired; run /antigravity login again')), timeoutMs);
|
|
60
|
+
timer.unref?.(); signal?.addEventListener('abort', abort, { once: true });
|
|
61
|
+
if (signal?.aborted) abort();
|
|
62
|
+
return { code, port: server.address()?.port, close: abort };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function tokenRequest(body, { fetch = globalThis.fetch, signal } = {}) {
|
|
66
|
+
const deadline = AbortSignal.timeout(15000);
|
|
67
|
+
try {
|
|
68
|
+
const response = await fetch(TOKEN_URL, { method: 'POST', redirect: 'error',
|
|
69
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
70
|
+
body: new URLSearchParams({ client_id: CLIENT_ID, client_secret: CLIENT_SECRET, ...body }),
|
|
71
|
+
signal: AbortSignal.any([deadline, ...(signal ? [signal] : [])]),
|
|
72
|
+
});
|
|
73
|
+
const data = await response.json();
|
|
74
|
+
if (!response.ok || typeof data.access_token !== 'string' || !data.access_token || !Number.isFinite(data.expires_in) || data.expires_in <= 0) {
|
|
75
|
+
throw new LlmError('Google could not authorize Antigravity; run /antigravity login again', response.status >= 500 ? 'SERVER' : 'AUTH');
|
|
76
|
+
}
|
|
77
|
+
return data;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (signal?.aborted) throw new LlmError('Antigravity login cancelled', 'ABORTED');
|
|
80
|
+
if (deadline.aborted) throw new LlmError('Google token request timed out', 'TIMEOUT');
|
|
81
|
+
if (error instanceof LlmError) throw error;
|
|
82
|
+
throw new LlmError('Could not contact Google for Antigravity authorization', 'TRANSPORT');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function exchangeCode(code, verifier, client, options = {}) {
|
|
87
|
+
const token = await tokenRequest({ code, code_verifier: verifier, grant_type: 'authorization_code', redirect_uri: REDIRECT_URI }, options);
|
|
88
|
+
if (typeof token.refresh_token !== 'string' || !token.refresh_token) throw new LlmError('Google did not issue an offline login; run /antigravity login again', 'AUTH');
|
|
89
|
+
const projectId = await client.project(token.access_token, options.signal);
|
|
90
|
+
return { version: 1, access: token.access_token, refresh: token.refresh_token, expires: Date.now() + token.expires_in * 1000, projectId };
|
|
91
|
+
}
|
|
92
|
+
export function parseGrant(raw) {
|
|
93
|
+
try {
|
|
94
|
+
const g = JSON.parse(raw);
|
|
95
|
+
return g?.version === 1 && ['access', 'refresh', 'projectId'].every(k => typeof g[k] === 'string' && g[k]) && Number.isFinite(g.expires) ? g : undefined;
|
|
96
|
+
} catch { return undefined; }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Serialize writes so a late callback or refresh cannot undo logout in this Host. */
|
|
100
|
+
export class AntigravitySession {
|
|
101
|
+
constructor(store, { fetch, now = Date.now } = {}) { this.store = store; this.fetch = fetch; this.now = now; this.revision = 0; this.writes = Promise.resolve(); }
|
|
102
|
+
current() { return this.store.read().then(parseGrant); }
|
|
103
|
+
write(action) { const next = this.writes.then(action); this.writes = next.catch(() => {}); return next; }
|
|
104
|
+
save(grant, revision = this.revision) {
|
|
105
|
+
return this.write(async () => {
|
|
106
|
+
if (revision !== this.revision) return false;
|
|
107
|
+
await this.store.write(JSON.stringify(grant)); return true;
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
logout() { this.revision++; return this.write(() => this.store.remove()); }
|
|
111
|
+
async auth() {
|
|
112
|
+
const grant = await this.current();
|
|
113
|
+
if (!grant) throw new LlmError('Sign in with /antigravity login first', 'MISSING_CREDENTIAL');
|
|
114
|
+
if (grant.expires - this.now() > MARGIN) return grant;
|
|
115
|
+
if (!this.pending) {
|
|
116
|
+
const revision = this.revision;
|
|
117
|
+
this.pending = (async () => {
|
|
118
|
+
const token = await tokenRequest({ grant_type: 'refresh_token', refresh_token: grant.refresh }, { fetch: this.fetch });
|
|
119
|
+
return this.write(async () => {
|
|
120
|
+
const current = await this.current();
|
|
121
|
+
if (!current || revision !== this.revision) throw new LlmError('Antigravity signed out during token refresh', 'MISSING_CREDENTIAL');
|
|
122
|
+
if (current.access !== grant.access || current.refresh !== grant.refresh) return current;
|
|
123
|
+
const next = { ...grant, access: token.access_token, refresh: token.refresh_token || grant.refresh, expires: this.now() + token.expires_in * 1000 };
|
|
124
|
+
await this.store.write(JSON.stringify(next)); return next;
|
|
125
|
+
});
|
|
126
|
+
})().finally(() => { this.pending = undefined; });
|
|
127
|
+
}
|
|
128
|
+
return this.pending;
|
|
129
|
+
}
|
|
130
|
+
}
|