Skip to content
Closed
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
2 changes: 1 addition & 1 deletion bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
} from "./plugin/request";
import { refreshAccessToken } from "./plugin/token";
import { startOAuthListener, type OAuthListener } from "./plugin/server";
import { setGlobalState } from "./plugin/state";
import { geminiQuota } from "./plugin/tools";
import type {
GetAuth,
LoaderResult,
Expand All @@ -36,9 +38,13 @@ import type {
export const GeminiCLIOAuthPlugin = async (
{ client }: PluginContext,
): Promise<PluginResult> => ({
tool: {
"gemini-quota": geminiQuota,
},
auth: {
provider: GEMINI_PROVIDER_ID,
loader: async (getAuth: GetAuth, provider: Provider): Promise<LoaderResult | null> => {
setGlobalState(getAuth, provider, client);
const auth = await getAuth();
if (!isOAuthAuth(auth)) {
return null;
Expand Down
17 changes: 17 additions & 0 deletions src/plugin/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { GetAuth, PluginClient, Provider } from "./types";

let currentGetAuth: GetAuth | undefined;
let currentProvider: Provider | undefined;
let currentClient: PluginClient | undefined;

export const setGlobalState = (getAuth: GetAuth, provider: Provider, client: PluginClient) => {
currentGetAuth = getAuth;
currentProvider = provider;
currentClient = client;
};

export const getGlobalState = () => ({
getAuth: currentGetAuth,
provider: currentProvider,
client: currentClient,
});
57 changes: 57 additions & 0 deletions src/plugin/tools.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { tool } from "@opencode-ai/plugin";
import { getGlobalState } from "./state";
import { isOAuthAuth, accessTokenExpired } from "./auth";
import { refreshAccessToken } from "./token";
import { ensureProjectContext } from "./project";
import { GEMINI_CODE_ASSIST_ENDPOINT } from "../constants";
import type { RetrieveUserQuotaResponse } from "./types";

export const geminiQuota = tool({
description: "Retrieves the current user's quota usage for Gemini models.",
args: {},
execute: async (_args, _ctx) => {
const { getAuth, client } = getGlobalState();

if (!getAuth || !client) {
throw new Error("Gemini plugin not initialized. Please ensure the provider is configured.");
}

let auth = await getAuth();
if (!isOAuthAuth(auth)) {
throw new Error("Quota retrieval is only available for OAuth authentication.");
}

if (accessTokenExpired(auth)) {
const refreshed = await refreshAccessToken(auth, client);
if (refreshed) {
auth = refreshed;
} else {
throw new Error("Failed to refresh access token.");
}
}

const projectContext = await ensureProjectContext(auth, client);
const projectId = projectContext.effectiveProjectId;

const url = `${GEMINI_CODE_ASSIST_ENDPOINT}/v1internal:retrieveUserQuota`;

const response = await fetch(url, {
method: "POST",
headers: {
"Authorization": `Bearer ${auth.access}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
project: projectId
})
});

if (!response.ok) {
const text = await response.text();
throw new Error(`Failed to retrieve quota: ${response.status} ${response.statusText} - ${text}`);
}

const data = await response.json() as RetrieveUserQuotaResponse;
return JSON.stringify(data, null, 2);
},
});
13 changes: 13 additions & 0 deletions src/plugin/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
import type { ToolDefinition } from "@opencode-ai/plugin";
import type { GeminiTokenExchangeResult } from "../gemini/oauth";

export interface QuotaBucket {
resetTime: string;
tokenType: string;
modelId: string;
remainingFraction: number;
}

export interface RetrieveUserQuotaResponse {
buckets: QuotaBucket[];
}

export interface OAuthAuthDetails {
type: "oauth";
refresh: string;
Expand Down Expand Up @@ -62,6 +74,7 @@ export interface PluginResult {
loader: (getAuth: GetAuth, provider: Provider) => Promise<LoaderResult | null>;
methods: AuthMethod[];
};
tool?: Record<string, ToolDefinition>;
}

export interface RefreshParts {
Expand Down