opencode-gemini-auth 1.3.9 → 1.3.11

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.
@@ -0,0 +1,187 @@
1
+ import { GEMINI_PROVIDER_ID } from "../../constants";
2
+ import { formatRefreshParts, parseRefreshParts } from "../auth";
3
+ import type { OAuthAuthDetails, PluginClient, ProjectContextResult } from "../types";
4
+ import { loadManagedProject, onboardManagedProject } from "./api";
5
+ import { FREE_TIER_ID, LEGACY_TIER_ID, ProjectIdRequiredError } from "./types";
6
+ import { buildIneligibleTierMessage, getCacheKey, normalizeProjectId, pickOnboardTier } from "./utils";
7
+
8
+ const projectContextResultCache = new Map<string, ProjectContextResult>();
9
+ const projectContextPendingCache = new Map<string, Promise<ProjectContextResult>>();
10
+
11
+ /**
12
+ * Clears cached project context results and pending promises.
13
+ */
14
+ export function invalidateProjectContextCache(refresh?: string): void {
15
+ if (!refresh) {
16
+ projectContextPendingCache.clear();
17
+ projectContextResultCache.clear();
18
+ return;
19
+ }
20
+
21
+ projectContextPendingCache.delete(refresh);
22
+ projectContextResultCache.delete(refresh);
23
+
24
+ const prefix = `${refresh}|cfg:`;
25
+ for (const key of projectContextPendingCache.keys()) {
26
+ if (key.startsWith(prefix)) {
27
+ projectContextPendingCache.delete(key);
28
+ }
29
+ }
30
+ for (const key of projectContextResultCache.keys()) {
31
+ if (key.startsWith(prefix)) {
32
+ projectContextResultCache.delete(key);
33
+ }
34
+ }
35
+ }
36
+
37
+ /**
38
+ * Resolves a project context for an access token, optionally persisting updated auth.
39
+ */
40
+ export async function resolveProjectContextFromAccessToken(
41
+ auth: OAuthAuthDetails,
42
+ accessToken: string,
43
+ configuredProjectId?: string,
44
+ persistAuth?: (auth: OAuthAuthDetails) => Promise<void>,
45
+ ): Promise<ProjectContextResult> {
46
+ const parts = parseRefreshParts(auth.refresh);
47
+ const projectId = configuredProjectId?.trim() || parts.projectId;
48
+
49
+ if (projectId || parts.managedProjectId) {
50
+ return {
51
+ auth,
52
+ effectiveProjectId: projectId || parts.managedProjectId || "",
53
+ };
54
+ }
55
+
56
+ const loadPayload = await loadManagedProject(accessToken, projectId);
57
+ if (!loadPayload) {
58
+ throw new ProjectIdRequiredError();
59
+ }
60
+
61
+ const managedProjectId = normalizeProjectId(loadPayload.cloudaicompanionProject);
62
+ if (managedProjectId) {
63
+ const updatedAuth = withProjectAuth(auth, parts.refreshToken, projectId, managedProjectId);
64
+ if (persistAuth) {
65
+ await persistAuth(updatedAuth);
66
+ }
67
+ return { auth: updatedAuth, effectiveProjectId: managedProjectId };
68
+ }
69
+
70
+ const currentTierId = loadPayload.currentTier?.id;
71
+ if (currentTierId) {
72
+ if (projectId) {
73
+ return { auth, effectiveProjectId: projectId };
74
+ }
75
+ const ineligibleMessage = buildIneligibleTierMessage(loadPayload.ineligibleTiers);
76
+ if (ineligibleMessage) {
77
+ throw new Error(ineligibleMessage);
78
+ }
79
+ throw new ProjectIdRequiredError();
80
+ }
81
+
82
+ const tier = pickOnboardTier(loadPayload.allowedTiers);
83
+ const tierId = tier.id ?? LEGACY_TIER_ID;
84
+ if (tierId !== FREE_TIER_ID && !projectId) {
85
+ throw new ProjectIdRequiredError();
86
+ }
87
+
88
+ const onboardedProjectId = await onboardManagedProject(accessToken, tierId, projectId);
89
+ if (onboardedProjectId) {
90
+ const updatedAuth = withProjectAuth(auth, parts.refreshToken, projectId, onboardedProjectId);
91
+ if (persistAuth) {
92
+ await persistAuth(updatedAuth);
93
+ }
94
+ return { auth: updatedAuth, effectiveProjectId: onboardedProjectId };
95
+ }
96
+
97
+ if (projectId) {
98
+ return { auth, effectiveProjectId: projectId };
99
+ }
100
+ throw new ProjectIdRequiredError();
101
+ }
102
+
103
+ /**
104
+ * Resolves an effective project ID for the current auth state, caching results per refresh token.
105
+ */
106
+ export async function ensureProjectContext(
107
+ auth: OAuthAuthDetails,
108
+ client: PluginClient,
109
+ configuredProjectId?: string,
110
+ ): Promise<ProjectContextResult> {
111
+ const accessToken = auth.access;
112
+ if (!accessToken) {
113
+ return { auth, effectiveProjectId: "" };
114
+ }
115
+
116
+ const cacheKey = buildProjectCacheKey(auth, configuredProjectId);
117
+ if (cacheKey) {
118
+ const cached = projectContextResultCache.get(cacheKey);
119
+ if (cached) {
120
+ return cached;
121
+ }
122
+ const pending = projectContextPendingCache.get(cacheKey);
123
+ if (pending) {
124
+ return pending;
125
+ }
126
+ }
127
+
128
+ const resolveContext = async (): Promise<ProjectContextResult> =>
129
+ resolveProjectContextFromAccessToken(
130
+ auth,
131
+ accessToken,
132
+ configuredProjectId,
133
+ async (updatedAuth) => {
134
+ await client.auth.set({
135
+ path: { id: GEMINI_PROVIDER_ID },
136
+ body: updatedAuth,
137
+ });
138
+ },
139
+ );
140
+
141
+ if (!cacheKey) {
142
+ return resolveContext();
143
+ }
144
+
145
+ const promise = resolveContext()
146
+ .then((result) => {
147
+ const nextKey = getCacheKey(result.auth) ?? cacheKey;
148
+ projectContextPendingCache.delete(cacheKey);
149
+ projectContextResultCache.set(nextKey, result);
150
+ if (nextKey !== cacheKey) {
151
+ projectContextResultCache.delete(cacheKey);
152
+ }
153
+ return result;
154
+ })
155
+ .catch((error) => {
156
+ projectContextPendingCache.delete(cacheKey);
157
+ throw error;
158
+ });
159
+
160
+ projectContextPendingCache.set(cacheKey, promise);
161
+ return promise;
162
+ }
163
+
164
+ function withProjectAuth(
165
+ auth: OAuthAuthDetails,
166
+ refreshToken: string,
167
+ projectId: string | undefined,
168
+ managedProjectId: string,
169
+ ): OAuthAuthDetails {
170
+ return {
171
+ ...auth,
172
+ refresh: formatRefreshParts({
173
+ refreshToken,
174
+ projectId,
175
+ managedProjectId,
176
+ }),
177
+ };
178
+ }
179
+
180
+ function buildProjectCacheKey(auth: OAuthAuthDetails, configuredProjectId?: string): string | undefined {
181
+ const base = getCacheKey(auth);
182
+ if (!base) {
183
+ return undefined;
184
+ }
185
+ const project = configuredProjectId?.trim() ?? "";
186
+ return project ? `${base}|cfg:${project}` : base;
187
+ }
@@ -0,0 +1,6 @@
1
+ export { loadManagedProject, onboardManagedProject, retrieveUserQuota } from "./api";
2
+ export {
3
+ ensureProjectContext,
4
+ invalidateProjectContextCache,
5
+ resolveProjectContextFromAccessToken,
6
+ } from "./context";
@@ -0,0 +1,55 @@
1
+ export const FREE_TIER_ID = "free-tier";
2
+ export const LEGACY_TIER_ID = "legacy-tier";
3
+
4
+ export const CODE_ASSIST_METADATA = {
5
+ ideType: "IDE_UNSPECIFIED",
6
+ platform: "PLATFORM_UNSPECIFIED",
7
+ pluginType: "GEMINI",
8
+ } as const;
9
+
10
+ export interface GeminiUserTier {
11
+ id?: string;
12
+ isDefault?: boolean;
13
+ userDefinedCloudaicompanionProject?: boolean;
14
+ name?: string;
15
+ description?: string;
16
+ }
17
+
18
+ export interface CloudAiCompanionProject {
19
+ id?: string;
20
+ }
21
+
22
+ export interface GeminiIneligibleTier {
23
+ reasonMessage?: string;
24
+ }
25
+
26
+ export interface LoadCodeAssistPayload {
27
+ cloudaicompanionProject?: string | CloudAiCompanionProject;
28
+ currentTier?: {
29
+ id?: string;
30
+ name?: string;
31
+ };
32
+ allowedTiers?: GeminiUserTier[];
33
+ ineligibleTiers?: GeminiIneligibleTier[];
34
+ }
35
+
36
+ export interface OnboardUserPayload {
37
+ name?: string;
38
+ done?: boolean;
39
+ response?: {
40
+ cloudaicompanionProject?: {
41
+ id?: string;
42
+ };
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Error raised when a required Google Cloud project is missing during Gemini onboarding.
48
+ */
49
+ export class ProjectIdRequiredError extends Error {
50
+ constructor() {
51
+ super(
52
+ "Google Gemini requires a Google Cloud project. Enable the Gemini for Google Cloud API on a project you control, then set `provider.google.options.projectId` in your Opencode config (or set OPENCODE_GEMINI_PROJECT_ID / GOOGLE_CLOUD_PROJECT).",
53
+ );
54
+ }
55
+ }
@@ -0,0 +1,120 @@
1
+ import type { OAuthAuthDetails } from "../types";
2
+ import {
3
+ CODE_ASSIST_METADATA,
4
+ LEGACY_TIER_ID,
5
+ type CloudAiCompanionProject,
6
+ type GeminiIneligibleTier,
7
+ type GeminiUserTier,
8
+ } from "./types";
9
+
10
+ /**
11
+ * Builds metadata headers required by the Code Assist API.
12
+ */
13
+ export function buildMetadata(projectId?: string, includeDuetProject = true): Record<string, string> {
14
+ const metadata: Record<string, string> = {
15
+ ideType: CODE_ASSIST_METADATA.ideType,
16
+ platform: CODE_ASSIST_METADATA.platform,
17
+ pluginType: CODE_ASSIST_METADATA.pluginType,
18
+ };
19
+ if (projectId && includeDuetProject) {
20
+ metadata.duetProject = projectId;
21
+ }
22
+ return metadata;
23
+ }
24
+
25
+ /**
26
+ * Normalizes project identifiers from API payloads or config.
27
+ */
28
+ export function normalizeProjectId(value?: string | CloudAiCompanionProject): string | undefined {
29
+ if (!value) {
30
+ return undefined;
31
+ }
32
+ if (typeof value === "string") {
33
+ const trimmed = value.trim();
34
+ return trimmed ? trimmed : undefined;
35
+ }
36
+ if (typeof value === "object" && typeof value.id === "string") {
37
+ const trimmed = value.id.trim();
38
+ return trimmed ? trimmed : undefined;
39
+ }
40
+ return undefined;
41
+ }
42
+
43
+ /**
44
+ * Selects the default tier ID from the allowed tiers list.
45
+ */
46
+ export function pickOnboardTier(allowedTiers?: GeminiUserTier[]): GeminiUserTier {
47
+ if (allowedTiers && allowedTiers.length > 0) {
48
+ for (const tier of allowedTiers) {
49
+ if (tier?.isDefault) {
50
+ return tier;
51
+ }
52
+ }
53
+ return allowedTiers[0] ?? { id: LEGACY_TIER_ID, userDefinedCloudaicompanionProject: true };
54
+ }
55
+ return { id: LEGACY_TIER_ID, userDefinedCloudaicompanionProject: true };
56
+ }
57
+
58
+ /**
59
+ * Builds a concise error message from ineligible tier payloads.
60
+ */
61
+ export function buildIneligibleTierMessage(tiers?: GeminiIneligibleTier[]): string | undefined {
62
+ if (!tiers || tiers.length === 0) {
63
+ return undefined;
64
+ }
65
+ const reasons = tiers
66
+ .map((tier) => tier?.reasonMessage?.trim())
67
+ .filter((message): message is string => !!message);
68
+ return reasons.length > 0 ? reasons.join(", ") : undefined;
69
+ }
70
+
71
+ /**
72
+ * Detects VPC-SC errors from Cloud Code responses.
73
+ */
74
+ export function isVpcScError(payload: unknown): boolean {
75
+ if (!payload || typeof payload !== "object") {
76
+ return false;
77
+ }
78
+ const error = (payload as { error?: unknown }).error;
79
+ if (!error || typeof error !== "object") {
80
+ return false;
81
+ }
82
+ const details = (error as { details?: unknown }).details;
83
+ if (!Array.isArray(details)) {
84
+ return false;
85
+ }
86
+ return details.some((detail) => {
87
+ if (!detail || typeof detail !== "object") {
88
+ return false;
89
+ }
90
+ return (detail as { reason?: unknown }).reason === "SECURITY_POLICY_VIOLATED";
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Safely parses JSON, returning null on failure.
96
+ */
97
+ export function parseJsonSafe(text: string): unknown {
98
+ try {
99
+ return JSON.parse(text);
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+
105
+ /**
106
+ * Promise-based delay utility.
107
+ */
108
+ export function wait(ms: number): Promise<void> {
109
+ return new Promise((resolve) => {
110
+ setTimeout(resolve, ms);
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Generates a cache key for project context based on refresh token.
116
+ */
117
+ export function getCacheKey(auth: OAuthAuthDetails): string | undefined {
118
+ const refresh = auth.refresh?.trim();
119
+ return refresh ? refresh : undefined;
120
+ }
@@ -0,0 +1,100 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import { isRecord, pickString } from "./shared";
4
+
5
+ const PROCESS_SESSION_ID = randomUUID();
6
+
7
+ function resolveUserPromptId(payload: Record<string, unknown>, request?: Record<string, unknown>): string {
8
+ const extra = isRecord(payload.extra_body) ? payload.extra_body : undefined;
9
+
10
+ return (
11
+ pickString(
12
+ payload.user_prompt_id,
13
+ payload.userPromptId,
14
+ payload.prompt_id,
15
+ payload.promptId,
16
+ payload.request_id,
17
+ payload.requestId,
18
+ request?.user_prompt_id,
19
+ request?.userPromptId,
20
+ request?.prompt_id,
21
+ request?.promptId,
22
+ request?.request_id,
23
+ request?.requestId,
24
+ extra?.user_prompt_id,
25
+ extra?.userPromptId,
26
+ extra?.prompt_id,
27
+ extra?.promptId,
28
+ extra?.request_id,
29
+ extra?.requestId,
30
+ ) ?? randomUUID()
31
+ );
32
+ }
33
+
34
+ function resolveSessionId(payload: Record<string, unknown>, request?: Record<string, unknown>): string {
35
+ const extra = isRecord(payload.extra_body) ? payload.extra_body : undefined;
36
+ return (
37
+ pickString(
38
+ request?.session_id,
39
+ request?.sessionId,
40
+ payload.session_id,
41
+ payload.sessionId,
42
+ extra?.session_id,
43
+ extra?.sessionId,
44
+ ) ?? PROCESS_SESSION_ID
45
+ );
46
+ }
47
+
48
+ function stripPromptIdentifierAliases(payload: Record<string, unknown>): void {
49
+ delete payload.user_prompt_id;
50
+ delete payload.userPromptId;
51
+ delete payload.prompt_id;
52
+ delete payload.promptId;
53
+ delete payload.request_id;
54
+ delete payload.requestId;
55
+ }
56
+
57
+ function stripSessionIdentifierAliases(payload: Record<string, unknown>): void {
58
+ delete payload.sessionId;
59
+ }
60
+
61
+ /**
62
+ * Applies canonical identifiers for wrapped Code Assist payloads.
63
+ *
64
+ * `user_prompt_id` and `session_id` are first-class identifiers in Gemini CLI
65
+ * request envelopes used for traceability, usage accounting, and support diagnostics.
66
+ */
67
+ export function normalizeWrappedIdentifiers(
68
+ wrapped: Record<string, unknown>,
69
+ ): { userPromptId: string; sessionId: string } {
70
+ const request = isRecord(wrapped.request) ? { ...wrapped.request } : {};
71
+ const userPromptId = resolveUserPromptId(wrapped, request);
72
+ const sessionId = resolveSessionId(wrapped, request);
73
+
74
+ request.session_id = sessionId;
75
+ stripSessionIdentifierAliases(request);
76
+ wrapped.request = request;
77
+
78
+ wrapped.user_prompt_id = userPromptId;
79
+ stripPromptIdentifierAliases(wrapped);
80
+
81
+ return { userPromptId, sessionId };
82
+ }
83
+
84
+ /**
85
+ * Applies canonical identifiers for unwrapped request payloads before wrapping.
86
+ *
87
+ * We normalize aliases here so downstream logic has a single source of truth.
88
+ */
89
+ export function normalizeRequestPayloadIdentifiers(
90
+ payload: Record<string, unknown>,
91
+ ): { userPromptId: string; sessionId: string } {
92
+ const userPromptId = resolveUserPromptId(payload);
93
+ const sessionId = resolveSessionId(payload);
94
+
95
+ payload.session_id = sessionId;
96
+ stripSessionIdentifierAliases(payload);
97
+ stripPromptIdentifierAliases(payload);
98
+
99
+ return { userPromptId, sessionId };
100
+ }
@@ -0,0 +1,3 @@
1
+ export { prepareGeminiRequest } from "./prepare";
2
+ export { transformGeminiResponse } from "./response";
3
+ export { isGenerativeLanguageRequest } from "./shared";
@@ -0,0 +1,128 @@
1
+ interface GeminiFunctionCallPart {
2
+ functionCall?: {
3
+ name: string;
4
+ args?: Record<string, unknown>;
5
+ [key: string]: unknown;
6
+ };
7
+ thoughtSignature?: string;
8
+ [key: string]: unknown;
9
+ }
10
+
11
+ interface OpenAIToolCall {
12
+ function?: {
13
+ name?: string;
14
+ arguments?: string;
15
+ [key: string]: unknown;
16
+ };
17
+ [key: string]: unknown;
18
+ }
19
+
20
+ interface OpenAIMessage {
21
+ content?: string | null;
22
+ tool_calls?: OpenAIToolCall[];
23
+ [key: string]: unknown;
24
+ }
25
+
26
+ /**
27
+ * Transforms OpenAI `tool_calls` to Gemini `functionCall` parts.
28
+ */
29
+ export function transformOpenAIToolCalls(requestPayload: Record<string, unknown>): void {
30
+ const messages = requestPayload.messages;
31
+ if (!messages || !Array.isArray(messages)) {
32
+ return;
33
+ }
34
+
35
+ for (const message of messages) {
36
+ if (!message || typeof message !== "object") {
37
+ continue;
38
+ }
39
+
40
+ const msgObj = message as OpenAIMessage;
41
+ const toolCalls = msgObj.tool_calls;
42
+ if (!toolCalls || !Array.isArray(toolCalls) || toolCalls.length === 0) {
43
+ continue;
44
+ }
45
+
46
+ const parts: GeminiFunctionCallPart[] = [];
47
+ if (typeof msgObj.content === "string" && msgObj.content.length > 0) {
48
+ parts.push({ text: msgObj.content });
49
+ }
50
+
51
+ for (const toolCall of toolCalls) {
52
+ if (!toolCall || typeof toolCall !== "object") {
53
+ continue;
54
+ }
55
+
56
+ const fn = toolCall.function;
57
+ if (!fn || typeof fn !== "object") {
58
+ continue;
59
+ }
60
+
61
+ const name = fn.name;
62
+ const args = parseJsonObject(fn.arguments);
63
+ parts.push({
64
+ functionCall: {
65
+ name: name ?? "",
66
+ args,
67
+ },
68
+ thoughtSignature: "skip_thought_signature_validator",
69
+ });
70
+ }
71
+
72
+ msgObj.parts = parts;
73
+ delete msgObj.tool_calls;
74
+ delete msgObj.content;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Adds synthetic thoughtSignature to function calls in both flat and wrapped payloads.
80
+ */
81
+ export function addThoughtSignaturesToFunctionCalls(requestPayload: Record<string, unknown>): void {
82
+ const processContents = (contents: unknown): void => {
83
+ if (!contents || !Array.isArray(contents)) {
84
+ return;
85
+ }
86
+
87
+ for (const content of contents) {
88
+ if (!content || typeof content !== "object") {
89
+ continue;
90
+ }
91
+
92
+ const parts = (content as Record<string, unknown>).parts;
93
+ if (!parts || !Array.isArray(parts)) {
94
+ continue;
95
+ }
96
+
97
+ for (const part of parts) {
98
+ if (!part || typeof part !== "object") {
99
+ continue;
100
+ }
101
+ const partObj = part as Record<string, unknown>;
102
+ if (partObj.functionCall && !partObj.thoughtSignature) {
103
+ partObj.thoughtSignature = "skip_thought_signature_validator";
104
+ }
105
+ }
106
+ }
107
+ };
108
+
109
+ processContents(requestPayload.contents);
110
+ if (requestPayload.request && typeof requestPayload.request === "object") {
111
+ processContents((requestPayload.request as Record<string, unknown>).contents);
112
+ }
113
+ }
114
+
115
+ function parseJsonObject(value: unknown): Record<string, unknown> {
116
+ if (typeof value !== "string") {
117
+ return {};
118
+ }
119
+ try {
120
+ const parsed = JSON.parse(value);
121
+ if (parsed && typeof parsed === "object") {
122
+ return parsed as Record<string, unknown>;
123
+ }
124
+ return {};
125
+ } catch {
126
+ return {};
127
+ }
128
+ }