opencode-gemini-auth 1.4.5 → 1.4.7
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.
- package/package.json +2 -2
- package/src/plugin/oauth-authorize.ts +7 -7
- package/src/plugin/project.test.ts +25 -0
- package/src/plugin/provider.test.ts +59 -0
- package/src/plugin/provider.ts +55 -0
- package/src/plugin/request/index.ts +1 -0
- package/src/plugin/request/prepare.ts +44 -5
- package/src/plugin/request-helpers/thinking.ts +8 -4
- package/src/plugin/request-helpers.test.ts +22 -1
- package/src/plugin/request.test.ts +76 -0
- package/src/plugin.ts +40 -20
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-gemini-auth",
|
|
3
3
|
"module": "index.ts",
|
|
4
|
-
"version": "1.4.
|
|
4
|
+
"version": "1.4.7",
|
|
5
5
|
"author": "jenslys",
|
|
6
6
|
"repository": "https://github.com/jenslys/opencode-gemini-auth",
|
|
7
7
|
"files": [
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"typescript": "^5.9.3"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@opencode-ai/plugin": "^1.2.
|
|
25
|
+
"@opencode-ai/plugin": "^1.2.20",
|
|
26
26
|
"@openauthjs/openauth": "^0.4.3"
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -4,13 +4,16 @@ import { authorizeGemini, exchangeGeminiWithVerifier } from "../gemini/oauth";
|
|
|
4
4
|
import type { GeminiTokenExchangeResult } from "../gemini/oauth";
|
|
5
5
|
import { isGeminiDebugEnabled, logGeminiDebugMessage } from "./debug";
|
|
6
6
|
import { resolveProjectContextFromAccessToken } from "./project";
|
|
7
|
+
import { resolveConfiguredProjectId } from "./provider";
|
|
7
8
|
import { startOAuthListener, type OAuthListener } from "./server";
|
|
8
9
|
import type { OAuthAuthDetails } from "./types";
|
|
9
10
|
|
|
10
11
|
/**
|
|
11
12
|
* Builds the OAuth authorize callback used by plugin auth methods.
|
|
12
13
|
*/
|
|
13
|
-
export function createOAuthAuthorizeMethod(
|
|
14
|
+
export function createOAuthAuthorizeMethod(options?: {
|
|
15
|
+
getConfiguredProjectId?: () => string | undefined;
|
|
16
|
+
}): () => Promise<{
|
|
14
17
|
url: string;
|
|
15
18
|
instructions: string;
|
|
16
19
|
method: string;
|
|
@@ -24,12 +27,9 @@ export function createOAuthAuthorizeMethod(): () => Promise<{
|
|
|
24
27
|
return result;
|
|
25
28
|
}
|
|
26
29
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
process.env.GOOGLE_CLOUD_PROJECT_ID?.trim() ??
|
|
31
|
-
"";
|
|
32
|
-
const configuredProjectId = projectFromEnv || googleProjectFromEnv || undefined;
|
|
30
|
+
const configuredProjectId = resolveConfiguredProjectId({
|
|
31
|
+
configProjectId: options?.getConfiguredProjectId?.(),
|
|
32
|
+
});
|
|
33
33
|
|
|
34
34
|
try {
|
|
35
35
|
const authSnapshot = {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { beforeEach, describe, expect, it, mock } from "bun:test";
|
|
2
2
|
|
|
3
|
+
import { formatRefreshParts } from "./auth";
|
|
3
4
|
import { resolveProjectContextFromAccessToken } from "./project";
|
|
4
5
|
import type { OAuthAuthDetails } from "./types";
|
|
5
6
|
|
|
@@ -109,4 +110,28 @@ describe("resolveProjectContextFromAccessToken", () => {
|
|
|
109
110
|
resolveProjectContextFromAccessToken(baseAuth, baseAuth.access ?? ""),
|
|
110
111
|
).rejects.toThrow("Google Gemini requires a Google Cloud project");
|
|
111
112
|
});
|
|
113
|
+
|
|
114
|
+
it("prefers a configured project id over a persisted managed project id", async () => {
|
|
115
|
+
const authWithManagedProject: OAuthAuthDetails = {
|
|
116
|
+
...baseAuth,
|
|
117
|
+
refresh: formatRefreshParts({
|
|
118
|
+
refreshToken: "refresh-token",
|
|
119
|
+
managedProjectId: "managed-project",
|
|
120
|
+
}),
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const fetchMock = mock(async () => {
|
|
124
|
+
throw new Error("should not fetch project context when a configured project id exists");
|
|
125
|
+
});
|
|
126
|
+
(globalThis as { fetch: typeof fetch }).fetch = fetchMock as unknown as typeof fetch;
|
|
127
|
+
|
|
128
|
+
const result = await resolveProjectContextFromAccessToken(
|
|
129
|
+
authWithManagedProject,
|
|
130
|
+
authWithManagedProject.access ?? "",
|
|
131
|
+
"configured-project",
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
expect(result.effectiveProjectId).toBe("configured-project");
|
|
135
|
+
expect(fetchMock.mock.calls.length).toBe(0);
|
|
136
|
+
});
|
|
112
137
|
});
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import type { Config } from "@opencode-ai/sdk";
|
|
3
|
+
|
|
4
|
+
import { resolveConfiguredProjectId, resolveConfiguredProjectIdFromConfig } from "./provider";
|
|
5
|
+
import type { Provider } from "./types";
|
|
6
|
+
|
|
7
|
+
describe("resolveConfiguredProjectId", () => {
|
|
8
|
+
it("reads project id from provider options", () => {
|
|
9
|
+
const provider = {
|
|
10
|
+
id: "google",
|
|
11
|
+
name: "Google",
|
|
12
|
+
source: "config",
|
|
13
|
+
env: [],
|
|
14
|
+
options: {
|
|
15
|
+
projectId: " provider-project ",
|
|
16
|
+
},
|
|
17
|
+
models: {},
|
|
18
|
+
} satisfies Provider;
|
|
19
|
+
|
|
20
|
+
expect(
|
|
21
|
+
resolveConfiguredProjectId({
|
|
22
|
+
provider,
|
|
23
|
+
env: {},
|
|
24
|
+
}),
|
|
25
|
+
).toBe("provider-project");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("falls back to the top-level config project id when provider options are unavailable", () => {
|
|
29
|
+
const config = {
|
|
30
|
+
provider: {
|
|
31
|
+
google: {
|
|
32
|
+
options: {
|
|
33
|
+
projectId: "config-project",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
} satisfies Config;
|
|
38
|
+
|
|
39
|
+
expect(resolveConfiguredProjectIdFromConfig(config)).toBe("config-project");
|
|
40
|
+
expect(
|
|
41
|
+
resolveConfiguredProjectId({
|
|
42
|
+
config,
|
|
43
|
+
env: {},
|
|
44
|
+
}),
|
|
45
|
+
).toBe("config-project");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("prefers OPENCODE_GEMINI_PROJECT_ID over config and google cloud env vars", () => {
|
|
49
|
+
expect(
|
|
50
|
+
resolveConfiguredProjectId({
|
|
51
|
+
configProjectId: "config-project",
|
|
52
|
+
env: {
|
|
53
|
+
OPENCODE_GEMINI_PROJECT_ID: "opencode-project",
|
|
54
|
+
GOOGLE_CLOUD_PROJECT: "google-project",
|
|
55
|
+
},
|
|
56
|
+
}),
|
|
57
|
+
).toBe("opencode-project");
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { Config } from "@opencode-ai/sdk";
|
|
2
|
+
|
|
3
|
+
import { GEMINI_PROVIDER_ID } from "../constants";
|
|
4
|
+
import type { Provider } from "./types";
|
|
5
|
+
|
|
6
|
+
interface ResolveConfiguredProjectIdInput {
|
|
7
|
+
provider?: Provider | null;
|
|
8
|
+
config?: Config | null;
|
|
9
|
+
configProjectId?: string;
|
|
10
|
+
env?: NodeJS.ProcessEnv;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveConfiguredProjectId(
|
|
14
|
+
input: ResolveConfiguredProjectIdInput = {},
|
|
15
|
+
): string | undefined {
|
|
16
|
+
const env = input.env ?? process.env;
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
normalizeProjectId(env.OPENCODE_GEMINI_PROJECT_ID) ??
|
|
20
|
+
resolveConfiguredProjectIdFromProvider(input.provider) ??
|
|
21
|
+
normalizeProjectId(input.configProjectId) ??
|
|
22
|
+
resolveConfiguredProjectIdFromConfig(input.config) ??
|
|
23
|
+
normalizeProjectId(env.GOOGLE_CLOUD_PROJECT) ??
|
|
24
|
+
normalizeProjectId(env.GOOGLE_CLOUD_PROJECT_ID)
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function resolveConfiguredProjectIdFromProvider(
|
|
29
|
+
provider: Provider | null | undefined,
|
|
30
|
+
): string | undefined {
|
|
31
|
+
if (!provider || typeof provider !== "object") {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
return normalizeProjectId(provider.options?.projectId);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function resolveConfiguredProjectIdFromConfig(
|
|
38
|
+
config: Config | null | undefined,
|
|
39
|
+
): string | undefined {
|
|
40
|
+
if (!config?.provider || typeof config.provider !== "object") {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const providerConfig = config.provider[GEMINI_PROVIDER_ID];
|
|
45
|
+
return normalizeProjectId(providerConfig?.options?.projectId);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeProjectId(value: unknown): string | undefined {
|
|
49
|
+
if (typeof value !== "string") {
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const trimmed = value.trim();
|
|
54
|
+
return trimmed || undefined;
|
|
55
|
+
}
|
|
@@ -12,6 +12,11 @@ const MODEL_FALLBACKS: Record<string, string> = {
|
|
|
12
12
|
"gemini-2.5-flash-image": "gemini-2.5-flash",
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
+
export interface ThinkingConfigDefaults {
|
|
16
|
+
provider?: unknown;
|
|
17
|
+
models?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
/**
|
|
16
21
|
* Rewrites OpenAI-style requests into Gemini Code Assist request shape.
|
|
17
22
|
*/
|
|
@@ -20,6 +25,7 @@ export function prepareGeminiRequest(
|
|
|
20
25
|
init: RequestInit | undefined,
|
|
21
26
|
accessToken: string,
|
|
22
27
|
projectId: string,
|
|
28
|
+
thinkingConfigDefaults?: ThinkingConfigDefaults,
|
|
23
29
|
): {
|
|
24
30
|
request: RequestInfo;
|
|
25
31
|
init: RequestInit;
|
|
@@ -61,7 +67,13 @@ export function prepareGeminiRequest(
|
|
|
61
67
|
let requestIdentifier: string = randomUUID();
|
|
62
68
|
|
|
63
69
|
if (typeof baseInit.body === "string" && baseInit.body) {
|
|
64
|
-
const transformed = transformRequestBody(
|
|
70
|
+
const transformed = transformRequestBody(
|
|
71
|
+
baseInit.body,
|
|
72
|
+
projectId,
|
|
73
|
+
effectiveModel,
|
|
74
|
+
rawModel,
|
|
75
|
+
thinkingConfigDefaults,
|
|
76
|
+
);
|
|
65
77
|
if (transformed.body) {
|
|
66
78
|
body = transformed.body;
|
|
67
79
|
requestIdentifier = transformed.userPromptId;
|
|
@@ -97,6 +109,8 @@ function transformRequestBody(
|
|
|
97
109
|
body: string,
|
|
98
110
|
projectId: string,
|
|
99
111
|
effectiveModel: string,
|
|
112
|
+
requestedModel: string,
|
|
113
|
+
thinkingConfigDefaults?: ThinkingConfigDefaults,
|
|
100
114
|
): { body?: string; userPromptId: string } {
|
|
101
115
|
const fallbackId = randomUUID();
|
|
102
116
|
try {
|
|
@@ -115,7 +129,11 @@ function transformRequestBody(
|
|
|
115
129
|
const requestPayload = { ...parsedBody };
|
|
116
130
|
transformOpenAIToolCalls(requestPayload);
|
|
117
131
|
addThoughtSignaturesToFunctionCalls(requestPayload);
|
|
118
|
-
normalizeThinking(
|
|
132
|
+
normalizeThinking(
|
|
133
|
+
requestPayload,
|
|
134
|
+
resolveDefaultThinkingConfig(thinkingConfigDefaults, requestedModel, effectiveModel),
|
|
135
|
+
thinkingConfigDefaults?.provider,
|
|
136
|
+
);
|
|
119
137
|
normalizeSystemInstruction(requestPayload);
|
|
120
138
|
normalizeCachedContent(requestPayload);
|
|
121
139
|
stripThoughtPartsFromHistory(requestPayload);
|
|
@@ -139,9 +157,30 @@ function transformRequestBody(
|
|
|
139
157
|
}
|
|
140
158
|
}
|
|
141
159
|
|
|
142
|
-
function
|
|
160
|
+
function resolveDefaultThinkingConfig(
|
|
161
|
+
thinkingConfigDefaults: ThinkingConfigDefaults | undefined,
|
|
162
|
+
requestedModel: string,
|
|
163
|
+
effectiveModel: string,
|
|
164
|
+
): unknown {
|
|
165
|
+
if (!thinkingConfigDefaults?.models) {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return thinkingConfigDefaults.models[requestedModel] ?? thinkingConfigDefaults.models[effectiveModel];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function normalizeThinking(
|
|
173
|
+
requestPayload: Record<string, unknown>,
|
|
174
|
+
modelThinkingConfig: unknown,
|
|
175
|
+
providerThinkingConfig: unknown,
|
|
176
|
+
): void {
|
|
143
177
|
const rawGenerationConfig = requestPayload.generationConfig as Record<string, unknown> | undefined;
|
|
144
|
-
const
|
|
178
|
+
const hasRequestThinkingConfig =
|
|
179
|
+
!!rawGenerationConfig && Object.prototype.hasOwnProperty.call(rawGenerationConfig, "thinkingConfig");
|
|
180
|
+
const sourceThinkingConfig = hasRequestThinkingConfig
|
|
181
|
+
? rawGenerationConfig?.thinkingConfig
|
|
182
|
+
: modelThinkingConfig ?? providerThinkingConfig;
|
|
183
|
+
const normalizedThinking = normalizeThinkingConfig(sourceThinkingConfig);
|
|
145
184
|
if (normalizedThinking) {
|
|
146
185
|
if (rawGenerationConfig) {
|
|
147
186
|
rawGenerationConfig.thinkingConfig = normalizedThinking;
|
|
@@ -152,7 +191,7 @@ function normalizeThinking(requestPayload: Record<string, unknown>): void {
|
|
|
152
191
|
return;
|
|
153
192
|
}
|
|
154
193
|
|
|
155
|
-
if (rawGenerationConfig
|
|
194
|
+
if (hasRequestThinkingConfig && rawGenerationConfig) {
|
|
156
195
|
delete rawGenerationConfig.thinkingConfig;
|
|
157
196
|
requestPayload.generationConfig = rawGenerationConfig;
|
|
158
197
|
}
|
|
@@ -14,13 +14,19 @@ export function normalizeThinkingConfig(config: unknown): ThinkingConfig | undef
|
|
|
14
14
|
const includeRaw = record.includeThoughts ?? record.include_thoughts;
|
|
15
15
|
|
|
16
16
|
const thinkingBudget = typeof budgetRaw === "number" && Number.isFinite(budgetRaw) ? budgetRaw : undefined;
|
|
17
|
-
const thinkingLevel =
|
|
17
|
+
const thinkingLevel =
|
|
18
|
+
typeof levelRaw === "string" && levelRaw.trim().length > 0 ? levelRaw.trim().toLowerCase() : undefined;
|
|
18
19
|
const includeThoughts = typeof includeRaw === "boolean" ? includeRaw : undefined;
|
|
19
20
|
|
|
20
21
|
if (thinkingBudget === undefined && thinkingLevel === undefined && includeThoughts === undefined) {
|
|
21
22
|
return undefined;
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
const thinkingEnabled =
|
|
26
|
+
(thinkingBudget !== undefined && thinkingBudget > 0) ||
|
|
27
|
+
thinkingLevel !== undefined;
|
|
28
|
+
const finalIncludeThoughts = thinkingEnabled ? includeThoughts ?? false : false;
|
|
29
|
+
|
|
24
30
|
const normalized: ThinkingConfig = {};
|
|
25
31
|
if (thinkingBudget !== undefined) {
|
|
26
32
|
normalized.thinkingBudget = thinkingBudget;
|
|
@@ -28,9 +34,7 @@ export function normalizeThinkingConfig(config: unknown): ThinkingConfig | undef
|
|
|
28
34
|
if (thinkingLevel !== undefined) {
|
|
29
35
|
normalized.thinkingLevel = thinkingLevel;
|
|
30
36
|
}
|
|
31
|
-
|
|
32
|
-
normalized.includeThoughts = includeThoughts;
|
|
33
|
-
}
|
|
37
|
+
normalized.includeThoughts = finalIncludeThoughts;
|
|
34
38
|
|
|
35
39
|
return normalized;
|
|
36
40
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
|
|
3
|
-
import { enhanceGeminiErrorResponse } from "./request-helpers";
|
|
3
|
+
import { enhanceGeminiErrorResponse, normalizeThinkingConfig } from "./request-helpers";
|
|
4
4
|
|
|
5
5
|
describe("enhanceGeminiErrorResponse", () => {
|
|
6
6
|
it("adds retry hint and rate-limit message for 429 rate limits", () => {
|
|
@@ -82,3 +82,24 @@ describe("enhanceGeminiErrorResponse", () => {
|
|
|
82
82
|
expect(result?.retryAfterMs).toBe(2000);
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
|
+
|
|
86
|
+
describe("normalizeThinkingConfig", () => {
|
|
87
|
+
it("forces includeThoughts to false when thinking is not enabled", () => {
|
|
88
|
+
expect(normalizeThinkingConfig({ includeThoughts: true })).toEqual({ includeThoughts: false });
|
|
89
|
+
expect(normalizeThinkingConfig({ thinkingBudget: 0, includeThoughts: true })).toEqual({
|
|
90
|
+
thinkingBudget: 0,
|
|
91
|
+
includeThoughts: false,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("keeps includeThoughts when thinking is enabled by budget or level", () => {
|
|
96
|
+
expect(normalizeThinkingConfig({ thinkingBudget: 8192, includeThoughts: true })).toEqual({
|
|
97
|
+
thinkingBudget: 8192,
|
|
98
|
+
includeThoughts: true,
|
|
99
|
+
});
|
|
100
|
+
expect(normalizeThinkingConfig({ thinkingLevel: "HIGH", includeThoughts: true })).toEqual({
|
|
101
|
+
thinkingLevel: "high",
|
|
102
|
+
includeThoughts: true,
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -122,4 +122,80 @@ describe("request helpers", () => {
|
|
|
122
122
|
expect(payload).toContain('"responseId":"trace-456"');
|
|
123
123
|
expect(payload).not.toContain('"traceId"');
|
|
124
124
|
});
|
|
125
|
+
|
|
126
|
+
it("injects model-level thinking defaults when request has no thinkingConfig", () => {
|
|
127
|
+
const input =
|
|
128
|
+
"https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent";
|
|
129
|
+
const init: RequestInit = {
|
|
130
|
+
method: "POST",
|
|
131
|
+
headers: {
|
|
132
|
+
"Content-Type": "application/json",
|
|
133
|
+
},
|
|
134
|
+
body: JSON.stringify({
|
|
135
|
+
contents: [{ role: "user", parts: [{ text: "hi" }] }],
|
|
136
|
+
}),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const result = prepareGeminiRequest(input, init, "token-123", "project-456", {
|
|
140
|
+
models: {
|
|
141
|
+
"gemini-3-flash-preview": {
|
|
142
|
+
thinkingLevel: "HIGH",
|
|
143
|
+
includeThoughts: true,
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
provider: {
|
|
147
|
+
thinkingLevel: "low",
|
|
148
|
+
includeThoughts: false,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const parsed = JSON.parse(result.init.body as string) as Record<string, unknown>;
|
|
153
|
+
const request = parsed.request as Record<string, unknown>;
|
|
154
|
+
const generationConfig = request.generationConfig as Record<string, unknown>;
|
|
155
|
+
expect(generationConfig.thinkingConfig).toEqual({
|
|
156
|
+
thinkingLevel: "high",
|
|
157
|
+
includeThoughts: true,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("prefers request thinkingConfig over model/provider defaults", () => {
|
|
162
|
+
const input =
|
|
163
|
+
"https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent";
|
|
164
|
+
const init: RequestInit = {
|
|
165
|
+
method: "POST",
|
|
166
|
+
headers: {
|
|
167
|
+
"Content-Type": "application/json",
|
|
168
|
+
},
|
|
169
|
+
body: JSON.stringify({
|
|
170
|
+
contents: [{ role: "user", parts: [{ text: "hi" }] }],
|
|
171
|
+
generationConfig: {
|
|
172
|
+
thinkingConfig: {
|
|
173
|
+
thinkingLevel: "low",
|
|
174
|
+
includeThoughts: false,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
const result = prepareGeminiRequest(input, init, "token-123", "project-456", {
|
|
181
|
+
models: {
|
|
182
|
+
"gemini-3-flash-preview": {
|
|
183
|
+
thinkingLevel: "high",
|
|
184
|
+
includeThoughts: true,
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
provider: {
|
|
188
|
+
thinkingLevel: "high",
|
|
189
|
+
includeThoughts: true,
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
const parsed = JSON.parse(result.init.body as string) as Record<string, unknown>;
|
|
194
|
+
const request = parsed.request as Record<string, unknown>;
|
|
195
|
+
const generationConfig = request.generationConfig as Record<string, unknown>;
|
|
196
|
+
expect(generationConfig.thinkingConfig).toEqual({
|
|
197
|
+
thinkingLevel: "low",
|
|
198
|
+
includeThoughts: false,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
125
201
|
});
|
package/src/plugin.ts
CHANGED
|
@@ -9,9 +9,11 @@ import {
|
|
|
9
9
|
} from "./plugin/quota";
|
|
10
10
|
import { isGeminiDebugEnabled, logGeminiDebugMessage, startGeminiDebugRequest } from "./plugin/debug";
|
|
11
11
|
import { maybeShowGeminiCapacityToast, maybeShowGeminiTestToast } from "./plugin/notify";
|
|
12
|
+
import { resolveConfiguredProjectId, resolveConfiguredProjectIdFromConfig } from "./plugin/provider";
|
|
12
13
|
import {
|
|
13
14
|
isGenerativeLanguageRequest,
|
|
14
15
|
prepareGeminiRequest,
|
|
16
|
+
type ThinkingConfigDefaults,
|
|
15
17
|
transformGeminiResponse,
|
|
16
18
|
} from "./plugin/request";
|
|
17
19
|
import { fetchWithRetry } from "./plugin/retry";
|
|
@@ -43,6 +45,7 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
43
45
|
{ client }: PluginContext,
|
|
44
46
|
): Promise<PluginResult> => ({
|
|
45
47
|
config: async (config) => {
|
|
48
|
+
latestGeminiConfiguredProjectId = resolveConfiguredProjectIdFromConfig(config);
|
|
46
49
|
config.command = config.command || {};
|
|
47
50
|
config.command[GEMINI_QUOTA_COMMAND] = {
|
|
48
51
|
description: "Show Gemini Code Assist quota usage",
|
|
@@ -65,9 +68,13 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
65
68
|
return null;
|
|
66
69
|
}
|
|
67
70
|
|
|
68
|
-
const configuredProjectId = resolveConfiguredProjectId(
|
|
71
|
+
const configuredProjectId = resolveConfiguredProjectId({
|
|
72
|
+
provider,
|
|
73
|
+
configProjectId: latestGeminiConfiguredProjectId,
|
|
74
|
+
});
|
|
69
75
|
latestGeminiConfiguredProjectId = configuredProjectId;
|
|
70
76
|
normalizeProviderModelCosts(provider);
|
|
77
|
+
const thinkingConfigDefaults = resolveThinkingConfigDefaults(provider);
|
|
71
78
|
|
|
72
79
|
return {
|
|
73
80
|
apiKey: "",
|
|
@@ -109,6 +116,7 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
109
116
|
init,
|
|
110
117
|
authRecord.access,
|
|
111
118
|
projectContext.effectiveProjectId,
|
|
119
|
+
thinkingConfigDefaults,
|
|
112
120
|
);
|
|
113
121
|
const debugContext = startGeminiDebugRequest({
|
|
114
122
|
originalUrl: toUrlString(input),
|
|
@@ -144,7 +152,9 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
144
152
|
{
|
|
145
153
|
label: "OAuth with Google (Gemini CLI)",
|
|
146
154
|
type: "oauth",
|
|
147
|
-
authorize: createOAuthAuthorizeMethod(
|
|
155
|
+
authorize: createOAuthAuthorizeMethod({
|
|
156
|
+
getConfiguredProjectId: () => latestGeminiConfiguredProjectId,
|
|
157
|
+
}),
|
|
148
158
|
},
|
|
149
159
|
{
|
|
150
160
|
provider: GEMINI_PROVIDER_ID,
|
|
@@ -158,24 +168,6 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
158
168
|
export const GoogleOAuthPlugin = GeminiCLIOAuthPlugin;
|
|
159
169
|
const loggedQuotaModelsByProject = new Set<string>();
|
|
160
170
|
|
|
161
|
-
function resolveConfiguredProjectId(provider: Provider): string | undefined {
|
|
162
|
-
const providerOptions =
|
|
163
|
-
provider && typeof provider === "object"
|
|
164
|
-
? ((provider as { options?: Record<string, unknown> }).options ?? undefined)
|
|
165
|
-
: undefined;
|
|
166
|
-
const projectIdFromConfig =
|
|
167
|
-
providerOptions && typeof providerOptions.projectId === "string"
|
|
168
|
-
? providerOptions.projectId.trim()
|
|
169
|
-
: "";
|
|
170
|
-
const projectIdFromEnv = process.env.OPENCODE_GEMINI_PROJECT_ID?.trim() ?? "";
|
|
171
|
-
const googleProjectIdFromEnv =
|
|
172
|
-
process.env.GOOGLE_CLOUD_PROJECT?.trim() ??
|
|
173
|
-
process.env.GOOGLE_CLOUD_PROJECT_ID?.trim() ??
|
|
174
|
-
"";
|
|
175
|
-
|
|
176
|
-
return projectIdFromEnv || projectIdFromConfig || googleProjectIdFromEnv || undefined;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
171
|
function normalizeProviderModelCosts(provider: Provider): void {
|
|
180
172
|
if (!provider.models) {
|
|
181
173
|
return;
|
|
@@ -187,6 +179,34 @@ function normalizeProviderModelCosts(provider: Provider): void {
|
|
|
187
179
|
}
|
|
188
180
|
}
|
|
189
181
|
|
|
182
|
+
function resolveThinkingConfigDefaults(provider: Provider): ThinkingConfigDefaults | undefined {
|
|
183
|
+
const providerOptions =
|
|
184
|
+
provider && typeof provider === "object"
|
|
185
|
+
? ((provider as { options?: Record<string, unknown> }).options ?? undefined)
|
|
186
|
+
: undefined;
|
|
187
|
+
const providerThinkingConfig = providerOptions?.thinkingConfig;
|
|
188
|
+
|
|
189
|
+
const modelThinkingConfigByModel: Record<string, unknown> = {};
|
|
190
|
+
for (const [modelId, model] of Object.entries(provider.models ?? {})) {
|
|
191
|
+
if (!model || typeof model !== "object") {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
const modelOptions = (model as { options?: Record<string, unknown> }).options;
|
|
195
|
+
if (modelOptions && typeof modelOptions === "object" && "thinkingConfig" in modelOptions) {
|
|
196
|
+
modelThinkingConfigByModel[modelId] = modelOptions.thinkingConfig;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (providerThinkingConfig === undefined && Object.keys(modelThinkingConfigByModel).length === 0) {
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
provider: providerThinkingConfig,
|
|
206
|
+
models: modelThinkingConfigByModel,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
190
210
|
async function ensureProjectContextOrThrow(
|
|
191
211
|
authRecord: OAuthAuthDetails,
|
|
192
212
|
client: PluginClient,
|