Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ jobs:
--no-extensions \
--extension "$package/extensions/file-search/index.ts" \
--extension "$package/extensions/git-info/index.ts" \
--extension "$package/extensions/ai-providers/index.ts" \
</dev/null

windows-background-terminals:
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ OpenPI 把成熟 Coding Agent 的工作习惯做成 Pi-native 能力,但不复
| 快捷工作流 | `/btw` 旁路提问(TUI)、`/lg` 浏览 Diff(TUI)、`/pr` 查 PR、`/copy-all`、`fd`、`rg`、只读 Git 工具 |
| 人类决策 | `ask_user` 草稿与最终复核、parent-only `human_handoff`、Plan Ready 实施门禁 |
| 统一配置 | `/openpi-setup` 管理 OpenPI 自有模型、并发、Footer、输出密度与 Post-edit 偏好 |
| 模型授权 | `/login google-antigravity`;实验性的 `/login cursor`(仅聊天,不执行 Cursor 原生工具) |

OpenPI 采用 [MIT License](LICENSE);第三方来源与保留声明见 [THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。

Expand Down Expand Up @@ -622,6 +623,7 @@ Plan Mode 不猜“任意 Shell 是否只读”,只放行由已知安全零件

```text
extensions/
├── ai-providers/ # Antigravity 与实验性 Cursor OAuth 模型 Provider
├── setup/ # /openpi-setup 与受限配置工具
├── capabilities/ # 最小能力发现入口与 Session 工具面加载
├── background-terminals/ # 长进程、日志、/ps
Expand Down Expand Up @@ -663,6 +665,6 @@ npm 仍用于发布包的 `pack` / clean-install 验证,因为用户通过 npm

本项目最初基于 [davis7dotsh/my-pi-setup](https://github.com/davis7dotsh/my-pi-setup) 演进,现作为独立发行版维护。感谢原作者提供起点。

`extensions/sessions/` 改编自 [jayshah5696/pi-agent-extensions](https://github.com/jayshah5696/pi-agent-extensions)。独立可选的顶层 Session 通信 package 见 [pi-intercom](https://github.com/nicobailon/pi-intercom)。完整第三方说明见 [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)。
`extensions/ai-providers/` 的部分协议实现改编自 [oh-my-pi](https://github.com/can1357/oh-my-pi);`extensions/sessions/` 改编自 [jayshah5696/pi-agent-extensions](https://github.com/jayshah5696/pi-agent-extensions)。独立可选的顶层 Session 通信 package 见 [pi-intercom](https://github.com/nicobailon/pi-intercom)。完整第三方说明见 [`THIRD_PARTY_NOTICES.md`](THIRD_PARTY_NOTICES.md)。

本项目以 MIT 许可证发布(见 [`LICENSE`](LICENSE));`THIRD_PARTY_NOTICES.md` 记录第三方来源与各自许可。
16 changes: 16 additions & 0 deletions THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Third-Party Notices

## OAuth model providers

`extensions/ai-providers/` adapts protocol and OAuth details from
[`oh-my-pi`](https://github.com/can1357/oh-my-pi) (Antigravity Cloud Code
Assist and Cursor AgentService). The upstream project is distributed under the
MIT License. Its copyright notice states `Copyright (c) 2025 Mario Zechner`,
`Copyright (c) 2025-2026 Can Bölük`, and `Copyright (c) 2026 Stencil Labs, Inc.`;
the complete license text is included at
[`extensions/ai-providers/LICENSE.upstream`](extensions/ai-providers/LICENSE.upstream).
The local Antigravity message conversion is adapted from the same project's
pi-ai 0.84.1 Google conversion implementation so the installed extension does
not depend on a non-public Pi runtime module.

Cursor support in this package is chat-only: it does not copy or execute
Cursor-native coding tools.

## Sessions extension

`extensions/sessions/` is adapted from
Expand Down
23 changes: 23 additions & 0 deletions extensions/ai-providers/LICENSE.upstream
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
MIT License

Copyright (c) 2025 Mario Zechner
Copyright (c) 2025-2026 Can Bölük
Copyright (c) 2026 Stencil Labs, Inc.

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
51 changes: 51 additions & 0 deletions extensions/ai-providers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# OAuth model providers

This extension registers two opt-in model providers backed by account OAuth:

- `google-antigravity` uses Google Cloud Code Assist and supports ordinary Pi
tool calls.
- `cursor` uses Cursor AgentService and is experimental, chat-only support.
It does not advertise or execute Cursor-native coding tools. If the server
requests one, the request fails explicitly instead of bypassing Pi's tool and
permission lifecycle.

After installing OpenPI, restart Pi or run `/reload`, then authenticate and
select a model:

```text
/login google-antigravity
/login cursor
/model
```

For source-checkout testing without changing the installed OpenPI package, load
this extension explicitly:

```sh
pi --approve -e ./extensions/ai-providers/index.ts
```

The model catalog is refreshed from the authenticated account and persisted by
Pi. A failed refresh retains the last successful account catalog; Antigravity
also has a validated static baseline, while Cursor keeps the server-side `Auto`
route as its baseline. No provider is contacted until it is selected.

Pi's interactive clipboard flow inserts an image's local path into the editor.
When Cursor is selected, a supported PNG/JPEG/GIF/WebP path at the start of an
interactive prompt is converted into an actual image attachment (up to 10 MiB)
before the request is sent. The absolute path is not exposed to the model.

The provider also adds an explicit chat-only rule so the normal Pi coding
system prompt cannot cause Cursor to attempt unavailable read or shell tools.

Cursor's token delta describes generated output only, so the provider does not
publish it as complete context usage. Pi 0.84.3+ can estimate an all-Cursor
history and trigger threshold compaction without provider usage. Older Pi hosts
retain the correct unknown-usage state but cannot automatically threshold-
compact a session with no usage-backed response. Project-wide Pi baseline
tracking is kept in [#328](https://github.com/openpi-dev/openpi/issues/328).

Cursor model discovery and chat use HTTP/2. They honor `PI_PROXY_CURSOR`, then
`PI_PROXY`, the standard `HTTPS_PROXY`/`HTTP_PROXY` variables, and `ALL_PROXY`;
`NO_PROXY` bypass rules apply. The proxy must support HTTP CONNECT and preserve
HTTP/2 ALPN negotiation to Cursor.
52 changes: 52 additions & 0 deletions extensions/ai-providers/antigravity/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* Credential codec for the Antigravity provider.
*
* pi persists whatever object `login` returns into auth.json and hands it back
* to `refreshToken`/`getApiKey` verbatim, so the Antigravity-specific extras
* (projectId, email) ride along as additional fields. `getApiKey` then packs
* everything the stream needs into the single `apiKey` string pi threads into
* `SimpleStreamOptions.apiKey`.
*/

import type { OAuthCredentials } from "@earendil-works/pi-ai/compat";

/** Stored credential shape: pi's OAuth fields plus Antigravity extras. */
export interface AntigravityCredentials extends OAuthCredentials {
/** Cloud Code Assist project resolved during login provisioning. */
projectId?: string;
/** Google account email, best-effort display metadata. */
email?: string;
}

/** What the stream function needs, packed into the apiKey string. */
export interface AntigravityApiKeyPayload {
token: string;
projectId?: string;
}

export function encodeApiKey(credentials: AntigravityCredentials): string {
const payload: AntigravityApiKeyPayload = {
token: credentials.access,
projectId: credentials.projectId,
};
return JSON.stringify(payload);
}

export function decodeApiKey(raw: string): AntigravityApiKeyPayload {
try {
const parsed: unknown = JSON.parse(raw);
if (parsed !== null && typeof parsed === "object" && "token" in parsed) {
const token = parsed.token;
if (typeof token === "string") {
const projectId =
"projectId" in parsed && typeof parsed.projectId === "string"
? parsed.projectId
: undefined;
return { token, projectId };
}
}
} catch {
// Not JSON: tolerate a bare access token (e.g. hand-written auth.json).
}
return { token: raw };
}
130 changes: 130 additions & 0 deletions extensions/ai-providers/antigravity/discovery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* Antigravity model discovery.
*
* `POST /v1internal:fetchAvailableModels` returns a map of wire model id to
* metadata (reference: omp packages/catalog/src/discovery/antigravity.ts).
* Static models are the provider baseline. Network/protocol failures throw so
* createProvider can retain the last successfully persisted dynamic catalog.
*/

import type { RefreshModelsContext } from "@earendil-works/pi-ai";
import { Type, type Static } from "typebox";
import { Value } from "typebox/value";
import { ensureAntigravityVersion, getAntigravityUserAgent } from "./oauth.ts";
import {
ANTIGRAVITY_API_URL,
type AntigravityProviderModel,
} from "./models.ts";
import {
collapseAntigravityModels,
type AntigravityModelDefinition,
} from "./routing.ts";

const DISCOVERY_ENDPOINTS = [
"https://daily-cloudcode-pa.googleapis.com",
"https://daily-cloudcode-pa.sandbox.googleapis.com",
] as const;
const FETCH_AVAILABLE_MODELS_PATH = "/v1internal:fetchAvailableModels";

// Reference: omp ANTIGRAVITY_DISCOVERY_DENYLIST.
const DISCOVERY_DENYLIST: Record<string, true> = {
chat_20706: true,
chat_23310: true,
"gemini-2.5-pro": true,
};

const DEFAULT_CONTEXT_WINDOW = 200_000;
const DEFAULT_MAX_TOKENS = 64_000;

const DiscoveryModelSchema = Type.Object({
displayName: Type.Optional(Type.String()),
supportsImages: Type.Optional(Type.Boolean()),
supportsThinking: Type.Optional(Type.Boolean()),
maxTokens: Type.Optional(Type.Number()),
maxOutputTokens: Type.Optional(Type.Number()),
isInternal: Type.Optional(Type.Boolean()),
});

const DiscoveryResponseSchema = Type.Object({
models: Type.Optional(Type.Record(Type.String(), DiscoveryModelSchema)),
});

type DiscoveryResponse = Static<typeof DiscoveryResponseSchema>;

type DiscoveredModel = AntigravityProviderModel & AntigravityModelDefinition;

function positiveNumber(value: number | undefined, fallback: number): number {
return value !== undefined && Number.isFinite(value) && value > 0
? value
: fallback;
}

function toModelDefinition(
id: string,
meta: Static<typeof DiscoveryModelSchema>,
): DiscoveredModel {
const advertisedMaxTokens = positiveNumber(
meta.maxOutputTokens,
DEFAULT_MAX_TOKENS,
);
return {
id,
name: meta.displayName ?? id,
api: "antigravity-cloudcode",
provider: "google-antigravity",
baseUrl: ANTIGRAVITY_API_URL,
reasoning: meta.supportsThinking === true,
input: meta.supportsImages === true ? ["text", "image"] : ["text"],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: positiveNumber(meta.maxTokens, DEFAULT_CONTEXT_WINDOW),
maxTokens: id.toLowerCase().includes("claude")
? Math.min(advertisedMaxTokens, DEFAULT_MAX_TOKENS)
: advertisedMaxTokens,
};
}

export async function fetchAntigravityModels(
context: RefreshModelsContext,
): Promise<DiscoveredModel[]> {
if (!context.allowNetwork) return [];
context.signal.throwIfAborted();
const credential = context.credential;
if (!credential || credential.type !== "oauth") return [];
await ensureAntigravityVersion(context.signal);

for (const endpoint of DISCOVERY_ENDPOINTS) {
if (context.signal.aborted) break;
try {
const response = await fetch(
`${endpoint}${FETCH_AVAILABLE_MODELS_PATH}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${credential.access}`,
"Content-Type": "application/json",
"User-Agent": getAntigravityUserAgent(),
},
body: "{}",
signal: context.signal,
},
);
if (!response.ok) continue;
const parsed = Value.Parse(
DiscoveryResponseSchema,
await response.json(),
) as DiscoveryResponse;
if (!parsed.models) continue;
const discovered = Object.entries(parsed.models)
.filter(
([id, meta]) =>
!Object.hasOwn(DISCOVERY_DENYLIST, id) && meta.isInternal !== true,
)
.map(([id, meta]) => toModelDefinition(id, meta));
return collapseAntigravityModels(discovered);
} catch {
// Try the next endpoint; total failure must preserve the stored catalog.
}
}
context.signal.throwIfAborted();
throw new Error("Antigravity model discovery failed on all endpoints");
}
Loading
Loading