mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-08 15:43:10 +08:00
feat(desktop): add plugin resource actions
This commit is contained in:
@@ -2,14 +2,14 @@ import type { JsonValue } from "cursor-byok:plugin";
|
||||
import type { ModelDefinition, ModelSnapshot, ModelSupport } from "cursor-byok:model";
|
||||
import { accountData } from "./resources.ts";
|
||||
|
||||
export const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
||||
export const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
||||
export const ANTIGRAVITY_PROD_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
||||
export const ANTIGRAVITY_DAILY_ENDPOINT = "https://daily-cloudcode-pa.googleapis.com";
|
||||
export const ANTIGRAVITY_SANDBOX_ENDPOINT = "https://daily-cloudcode-pa.sandbox.googleapis.com";
|
||||
|
||||
export const ANTIGRAVITY_ENDPOINTS = [
|
||||
ANTIGRAVITY_SANDBOX_ENDPOINT,
|
||||
ANTIGRAVITY_DAILY_ENDPOINT,
|
||||
ANTIGRAVITY_PROD_ENDPOINT,
|
||||
ANTIGRAVITY_DAILY_ENDPOINT,
|
||||
ANTIGRAVITY_SANDBOX_ENDPOINT,
|
||||
];
|
||||
|
||||
const FETCH_AVAILABLE_MODELS_PATH = "/v1internal:fetchAvailableModels";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"apiVersion": 1,
|
||||
"id": "dev.cursorbyok.plugins.antigravity-auth",
|
||||
"name": "Antigravity",
|
||||
"version": "0.1.7",
|
||||
"name": "Google",
|
||||
"version": "0.1.8",
|
||||
"author": "@masudranaxpert",
|
||||
"minAppVersion": "0.1.0",
|
||||
"icon": "assets/antigravity.svg",
|
||||
|
||||
@@ -357,6 +357,7 @@ async function streamCloudCode(
|
||||
const headers: Record<string, string> = {
|
||||
authorization: `Bearer ${accessToken}`,
|
||||
"content-type": "application/json",
|
||||
accept: "text/event-stream",
|
||||
"user-agent": ANTIGRAVITY_USER_AGENT,
|
||||
...ANTIGRAVITY_CLIENT_HEADERS,
|
||||
};
|
||||
@@ -403,6 +404,7 @@ async function streamCloudCode(
|
||||
inputTokens: number | null;
|
||||
outputTokens: number | null;
|
||||
totalTokens: number | null;
|
||||
cacheReadTokens: number | null;
|
||||
} | null = null;
|
||||
|
||||
for await (const line of response.lines) {
|
||||
@@ -428,6 +430,9 @@ async function streamCloudCode(
|
||||
? usage.candidatesTokenCount
|
||||
: null,
|
||||
totalTokens: typeof usage.totalTokenCount === "number" ? usage.totalTokenCount : null,
|
||||
cacheReadTokens: typeof usage.cachedContentTokenCount === "number"
|
||||
? usage.cachedContentTokenCount
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -535,7 +540,7 @@ async function streamCloudCode(
|
||||
inputTokens: finalUsage.inputTokens,
|
||||
outputTokens: finalUsage.outputTokens,
|
||||
totalTokens: finalUsage.totalTokens,
|
||||
cacheReadTokens: null,
|
||||
cacheReadTokens: finalUsage.cacheReadTokens,
|
||||
cacheWriteTokens: null,
|
||||
reasoningTokens: null,
|
||||
},
|
||||
@@ -573,7 +578,7 @@ async function streamCloudCode(
|
||||
inputTokens: finalUsage.inputTokens,
|
||||
outputTokens: finalUsage.outputTokens,
|
||||
totalTokens: finalUsage.totalTokens,
|
||||
cacheReadTokens: null,
|
||||
cacheReadTokens: finalUsage.cacheReadTokens,
|
||||
cacheWriteTokens: null,
|
||||
reasoningTokens: null,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import type {
|
||||
JsonValue,
|
||||
NetworkEventStream,
|
||||
NetworkResponse,
|
||||
PluginContext,
|
||||
} from "cursor-byok:plugin";
|
||||
import type { LlmRequest, ModelEvent } from "cursor-byok:provider";
|
||||
import type { ResourceSnapshot } from "cursor-byok:resource";
|
||||
import { antigravityProvider, isQuotaError } from "./provider.ts";
|
||||
import { RESOURCE_TYPE } from "./resources.ts";
|
||||
|
||||
function assert(condition: unknown, message = "assertion failed"): asserts condition {
|
||||
if (!condition) throw new Error(message);
|
||||
}
|
||||
|
||||
function assertEquals(actual: unknown, expected: unknown): void {
|
||||
const left = JSON.stringify(actual);
|
||||
const right = JSON.stringify(expected);
|
||||
if (left !== right) throw new Error(`expected ${right}, received ${left}`);
|
||||
}
|
||||
|
||||
type RequestInit = { body?: string; headers?: Record<string, string> };
|
||||
type FetchHandler = (url: string, init?: RequestInit) => NetworkResponse;
|
||||
type StreamHandler = (url: string, init?: RequestInit) => NetworkEventStream;
|
||||
|
||||
function context(handlers: { fetch?: FetchHandler; stream?: StreamHandler }): PluginContext {
|
||||
return {
|
||||
network: {
|
||||
fetch: (url, init) => {
|
||||
if (!handlers.fetch) throw new Error("fetch was not expected");
|
||||
return Promise.resolve(handlers.fetch(url, init));
|
||||
},
|
||||
stream: (url, init) => {
|
||||
if (!handlers.stream) throw new Error("stream was not expected");
|
||||
return Promise.resolve(handlers.stream(url, init));
|
||||
},
|
||||
},
|
||||
signal: new AbortController().signal,
|
||||
};
|
||||
}
|
||||
|
||||
function snapshot(privateData: JsonValue): ResourceSnapshot {
|
||||
return {
|
||||
id: "resource-1",
|
||||
type: RESOURCE_TYPE,
|
||||
key: "antigravity:user-1",
|
||||
privateData,
|
||||
state: { status: "ready" },
|
||||
};
|
||||
}
|
||||
|
||||
async function* sse(lines: string[]): AsyncGenerator<string> {
|
||||
for (const line of lines) yield line;
|
||||
}
|
||||
|
||||
function request(): LlmRequest {
|
||||
return {
|
||||
instructions: "You are an AI coding assistant.",
|
||||
messages: [{ role: "user", content: [{ type: "text", text: "Hello" }] }],
|
||||
tools: [],
|
||||
reasoning: { enabled: true, effort: "medium" },
|
||||
latency: "standard",
|
||||
maxOutputTokens: 65536,
|
||||
cacheKey: "conv-1",
|
||||
};
|
||||
}
|
||||
|
||||
Deno.test("provider parses usage metadata including cachedContentTokenCount as cacheReadTokens", async () => {
|
||||
let requestBody = "";
|
||||
let requestHeaders: Record<string, string> = {};
|
||||
const events: ModelEvent[] = [];
|
||||
|
||||
const result = await antigravityProvider.invoke(
|
||||
{
|
||||
model: {
|
||||
id: "gemini-3.7-flash-medium",
|
||||
displayName: "Gemini 3.7 Flash Medium",
|
||||
privateData: {},
|
||||
},
|
||||
resource: snapshot({
|
||||
accessToken: "mock-access-token",
|
||||
refreshToken: "mock-refresh-token",
|
||||
expiresAt: Date.now() + 3600_000,
|
||||
projectId: "test-project-123",
|
||||
}),
|
||||
request: request(),
|
||||
},
|
||||
{ emit: (event) => events.push(event) },
|
||||
context({
|
||||
stream: (url, init) => {
|
||||
assert(url.includes("/v1internal:streamGenerateContent?alt=sse"));
|
||||
requestBody = init?.body ?? "";
|
||||
requestHeaders = init?.headers ?? {};
|
||||
return {
|
||||
status: 200,
|
||||
headers: {},
|
||||
lines: sse([
|
||||
'data: {"response":{"candidates":[{"content":{"parts":[{"thought":true,"text":"Thinking..."}]}}]}}',
|
||||
'data: {"response":{"candidates":[{"content":{"parts":[{"text":"Hello world"}]}}]}}',
|
||||
'data: {"response":{"candidates":[{"finishReason":"STOP"}],"usageMetadata":{"promptTokenCount":120,"candidatesTokenCount":15,"totalTokenCount":135,"cachedContentTokenCount":80}}}',
|
||||
]),
|
||||
};
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
assertEquals(result, { status: "completed" });
|
||||
assertEquals(requestHeaders["authorization"], "Bearer mock-access-token");
|
||||
assertEquals(requestHeaders.accept, "text/event-stream");
|
||||
assert(!("session-id" in requestHeaders), "Codex-only session-id header was sent");
|
||||
assert(!("thread-id" in requestHeaders), "Codex-only thread-id header was sent");
|
||||
assert(!("x-client-request-id" in requestHeaders), "request-id header was reused as cache key");
|
||||
const body = JSON.parse(requestBody) as Record<string, unknown>;
|
||||
assertEquals(body.project, "test-project-123");
|
||||
assertEquals(body.model, "gemini-3.7-flash-medium");
|
||||
assert(!("sessionId" in body), "unsupported sessionId field was sent");
|
||||
assert(!("conversationId" in body), "unsupported conversationId field was sent");
|
||||
|
||||
assertEquals(events, [
|
||||
{ type: "thinking-start" },
|
||||
{ type: "thinking-delta", text: "Thinking..." },
|
||||
{ type: "thinking-end" },
|
||||
{ type: "text-start" },
|
||||
{ type: "text-delta", text: "Hello world" },
|
||||
{ type: "text-end" },
|
||||
{
|
||||
type: "usage",
|
||||
usage: {
|
||||
inputTokens: 120,
|
||||
outputTokens: 15,
|
||||
totalTokens: 135,
|
||||
cacheReadTokens: 80,
|
||||
cacheWriteTokens: null,
|
||||
reasoningTokens: null,
|
||||
},
|
||||
},
|
||||
{ type: "done", reason: "stop" },
|
||||
]);
|
||||
});
|
||||
|
||||
Deno.test("isQuotaError identifies rate limits and quota exhaustion", () => {
|
||||
assert(isQuotaError("RESOURCE_EXHAUSTED: quota exceeded"));
|
||||
assert(isQuotaError("Rate limit exceeded for model"));
|
||||
assert(isQuotaError("HTTP 429 Too Many Requests"));
|
||||
assert(!isQuotaError("Invalid authorization header"));
|
||||
});
|
||||
Reference in New Issue
Block a user