Skip to content

Commit bd88e23

Browse files
committed
feat: normalize thinkingConfig
- Expose Gemini cache hits and allow cached_content passthrough - Normalize thinkingConfig to avoid invalid includeThoughts errors - Cleanup
1 parent f7cb08a commit bd88e23

File tree

11 files changed

+380
-112
lines changed

11 files changed

+380
-112
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "opencode-gemini-auth",
33
"module": "index.ts",
4-
"version": "1.1.5",
4+
"version": "1.1.6",
55
"author": "jenslys",
66
"repository": "https://github.com/jenslys/opencode-gemini-auth",
77
"files": [

src/plugin.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ import type {
2121
Provider,
2222
} from "./plugin/types";
2323

24+
/**
25+
* Registers the Gemini OAuth provider for Opencode, handling auth, request rewriting,
26+
* debug logging, and response normalization for Gemini Code Assist endpoints.
27+
*/
2428
export const GeminiCLIOAuthPlugin = async (
2529
{ client }: PluginContext,
2630
): Promise<PluginResult> => ({
@@ -66,6 +70,9 @@ export const GeminiCLIOAuthPlugin = async (
6670
return fetch(input, init);
6771
}
6872

73+
/**
74+
* Ensures we have a usable project context for the current auth snapshot.
75+
*/
6976
async function resolveProjectContext(): Promise<ProjectContextResult> {
7077
try {
7178
return await ensureProjectContext(authRecord, client);
@@ -115,7 +122,6 @@ export const GeminiCLIOAuthPlugin = async (
115122
authorize: async () => {
116123
console.log("\n=== Google Gemini OAuth Setup ===");
117124

118-
// Detect headless/SSH environment
119125
const isHeadless = !!(
120126
process.env.SSH_CONNECTION ||
121127
process.env.SSH_CLIENT ||
@@ -191,7 +197,6 @@ export const GeminiCLIOAuthPlugin = async (
191197
try {
192198
await listener?.close();
193199
} catch {
194-
// Ignore close errors.
195200
}
196201
}
197202
},

src/plugin/auth.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ export function isOAuthAuth(auth: AuthDetails): auth is OAuthAuthDetails {
66
return auth.type === "oauth";
77
}
88

9+
/**
10+
* Splits a packed refresh string into its constituent refresh token and project IDs.
11+
*/
912
export function parseRefreshParts(refresh: string): RefreshParts {
1013
const [refreshToken = "", projectId = "", managedProjectId = ""] = (refresh ?? "").split("|");
1114
return {
@@ -15,12 +18,18 @@ export function parseRefreshParts(refresh: string): RefreshParts {
1518
};
1619
}
1720

21+
/**
22+
* Serializes refresh token parts into the stored string format.
23+
*/
1824
export function formatRefreshParts(parts: RefreshParts): string {
1925
const projectSegment = parts.projectId ?? "";
2026
const base = `${parts.refreshToken}|${projectSegment}`;
2127
return parts.managedProjectId ? `${base}|${parts.managedProjectId}` : base;
2228
}
2329

30+
/**
31+
* Determines whether an access token is expired or missing, with buffer for clock skew.
32+
*/
2433
export function accessTokenExpired(auth: OAuthAuthDetails): boolean {
2534
if (!auth.access || typeof auth.expires !== "number") {
2635
return true;

src/plugin/cache.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ import type { OAuthAuthDetails } from "./types";
33

44
const authCache = new Map<string, OAuthAuthDetails>();
55

6+
/**
7+
* Produces a stable cache key from a refresh token string.
8+
*/
69
function normalizeRefreshKey(refresh?: string): string | undefined {
710
const key = refresh?.trim();
811
return key ? key : undefined;
912
}
1013

14+
/**
15+
* Returns a cached auth snapshot when available, favoring unexpired tokens.
16+
*/
1117
export function resolveCachedAuth(auth: OAuthAuthDetails): OAuthAuthDetails {
1218
const key = normalizeRefreshKey(auth.refresh);
1319
if (!key) {
@@ -33,6 +39,9 @@ export function resolveCachedAuth(auth: OAuthAuthDetails): OAuthAuthDetails {
3339
return auth;
3440
}
3541

42+
/**
43+
* Stores the latest auth snapshot keyed by refresh token.
44+
*/
3645
export function storeCachedAuth(auth: OAuthAuthDetails): void {
3746
const key = normalizeRefreshKey(auth.refresh);
3847
if (!key) {
@@ -41,6 +50,9 @@ export function storeCachedAuth(auth: OAuthAuthDetails): void {
4150
authCache.set(key, auth);
4251
}
4352

53+
/**
54+
* Clears cached auth globally or for a specific refresh token.
55+
*/
4456
export function clearCachedAuth(refresh?: string): void {
4557
if (!refresh) {
4658
authCache.clear();

src/plugin/cli.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { createInterface } from "node:readline/promises";
22
import { stdin as input, stdout as output } from "node:process";
33

4+
/**
5+
* Prompts the user for a project ID via stdin/stdout.
6+
*/
47
export async function promptProjectId(): Promise<string> {
58
const rl = createInterface({ input, output });
69
try {

src/plugin/debug.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,14 @@ interface GeminiDebugResponseMeta {
2828
body?: string;
2929
note?: string;
3030
error?: unknown;
31+
headersOverride?: HeadersInit;
3132
}
3233

3334
let requestCounter = 0;
3435

36+
/**
37+
* Begins a debug trace for a Gemini request, logging request metadata when debugging is enabled.
38+
*/
3539
export function startGeminiDebugRequest(meta: GeminiDebugRequestMeta): GeminiDebugContext | null {
3640
if (!debugEnabled) {
3741
return null;
@@ -56,6 +60,9 @@ export function startGeminiDebugRequest(meta: GeminiDebugRequestMeta): GeminiDeb
5660
return { id, streaming: meta.streaming, startedAt: Date.now() };
5761
}
5862

63+
/**
64+
* Logs response details for a previously started debug trace when debugging is enabled.
65+
*/
5966
export function logGeminiDebugResponse(
6067
context: GeminiDebugContext | null | undefined,
6168
response: Response,
@@ -70,7 +77,9 @@ export function logGeminiDebugResponse(
7077
`[Gemini Debug ${context.id}] Response ${response.status} ${response.statusText} (${durationMs}ms)`,
7178
);
7279
logDebug(
73-
`[Gemini Debug ${context.id}] Response Headers: ${JSON.stringify(maskHeaders(response.headers))}`,
80+
`[Gemini Debug ${context.id}] Response Headers: ${JSON.stringify(
81+
maskHeaders(meta.headersOverride ?? response.headers),
82+
)}`,
7483
);
7584

7685
if (meta.note) {
@@ -88,6 +97,9 @@ export function logGeminiDebugResponse(
8897
}
8998
}
9099

100+
/**
101+
* Obscures sensitive headers and returns a plain object for logging.
102+
*/
91103
function maskHeaders(headers?: HeadersInit | Headers): Record<string, string> {
92104
if (!headers) {
93105
return {};
@@ -105,6 +117,9 @@ function maskHeaders(headers?: HeadersInit | Headers): Record<string, string> {
105117
return result;
106118
}
107119

120+
/**
121+
* Produces a short, type-aware preview of a request/response body for logs.
122+
*/
108123
function formatBodyPreview(body?: BodyInit | null): string | undefined {
109124
if (body == null) {
110125
return undefined;
@@ -129,17 +144,26 @@ function formatBodyPreview(body?: BodyInit | null): string | undefined {
129144
return `[${body.constructor?.name ?? typeof body} payload omitted]`;
130145
}
131146

147+
/**
148+
* Truncates long strings to a fixed preview length for logging.
149+
*/
132150
function truncateForLog(text: string): string {
133151
if (text.length <= MAX_BODY_PREVIEW_CHARS) {
134152
return text;
135153
}
136154
return `${text.slice(0, MAX_BODY_PREVIEW_CHARS)}... (truncated ${text.length - MAX_BODY_PREVIEW_CHARS} chars)`;
137155
}
138156

157+
/**
158+
* Writes a single debug line using the configured writer.
159+
*/
139160
function logDebug(line: string): void {
140161
logWriter(line);
141162
}
142163

164+
/**
165+
* Converts unknown error-like values into printable strings.
166+
*/
143167
function formatError(error: unknown): string {
144168
if (error instanceof Error) {
145169
return error.stack ?? error.message;
@@ -151,11 +175,17 @@ function formatError(error: unknown): string {
151175
}
152176
}
153177

178+
/**
179+
* Builds a timestamped log file path in the current working directory.
180+
*/
154181
function defaultLogFilePath(): string {
155182
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
156183
return join(cwd(), `gemini-debug-${timestamp}.log`);
157184
}
158185

186+
/**
187+
* Creates a line writer that appends to a file when provided.
188+
*/
159189
function createLogWriter(filePath?: string): (line: string) => void {
160190
if (!filePath) {
161191
return () => {};

src/plugin/project.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,13 +43,19 @@ interface OnboardUserPayload {
4343
}
4444

4545
class ProjectIdRequiredError extends Error {
46+
/**
47+
* Error raised when a required Google Cloud project is missing during Gemini onboarding.
48+
*/
4649
constructor() {
4750
super(
4851
"Google Gemini requires a Google Cloud project. Enable the Gemini for Google Cloud API on a project you control, rerun `opencode auth login`, and supply that project ID when prompted.",
4952
);
5053
}
5154
}
5255

56+
/**
57+
* Builds metadata headers required by the Code Assist API.
58+
*/
5359
function buildMetadata(projectId?: string): Record<string, string> {
5460
const metadata: Record<string, string> = {
5561
ideType: CODE_ASSIST_METADATA.ideType,
@@ -62,6 +68,9 @@ function buildMetadata(projectId?: string): Record<string, string> {
6268
return metadata;
6369
}
6470

71+
/**
72+
* Selects the default tier ID from the allowed tiers list.
73+
*/
6574
function getDefaultTierId(allowedTiers?: GeminiUserTier[]): string | undefined {
6675
if (!allowedTiers || allowedTiers.length === 0) {
6776
return undefined;
@@ -74,17 +83,26 @@ function getDefaultTierId(allowedTiers?: GeminiUserTier[]): string | undefined {
7483
return allowedTiers[0]?.id;
7584
}
7685

86+
/**
87+
* Promise-based delay utility.
88+
*/
7789
function wait(ms: number): Promise<void> {
7890
return new Promise(function (resolve) {
7991
setTimeout(resolve, ms);
8092
});
8193
}
8294

95+
/**
96+
* Generates a cache key for project context based on refresh token.
97+
*/
8398
function getCacheKey(auth: OAuthAuthDetails): string | undefined {
8499
const refresh = auth.refresh?.trim();
85100
return refresh ? refresh : undefined;
86101
}
87102

103+
/**
104+
* Clears cached project context results and pending promises, globally or for a refresh key.
105+
*/
88106
export function invalidateProjectContextCache(refresh?: string): void {
89107
if (!refresh) {
90108
projectContextPendingCache.clear();
@@ -95,6 +113,9 @@ export function invalidateProjectContextCache(refresh?: string): void {
95113
projectContextResultCache.delete(refresh);
96114
}
97115

116+
/**
117+
* Loads managed project information for the given access token and optional project.
118+
*/
98119
export async function loadManagedProject(
99120
accessToken: string,
100121
projectId?: string,
@@ -132,6 +153,9 @@ export async function loadManagedProject(
132153
}
133154

134155

156+
/**
157+
* Onboards a managed project for the user, optionally retrying until completion.
158+
*/
135159
export async function onboardManagedProject(
136160
accessToken: string,
137161
tierId: string,
@@ -190,6 +214,9 @@ export async function onboardManagedProject(
190214
return undefined;
191215
}
192216

217+
/**
218+
* Resolves an effective project ID for the current auth state, caching results per refresh token.
219+
*/
193220
export async function ensureProjectContext(
194221
auth: OAuthAuthDetails,
195222
client: PluginClient,

0 commit comments

Comments
 (0)