opencode-gemini-auth 1.4.4 → 1.4.6
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 +4 -2
- package/src/constants.ts +0 -1
- package/src/plugin/gemini-cli-version.ts +5 -0
- package/src/plugin/project/api.ts +4 -0
- package/src/plugin/request/index.ts +1 -0
- package/src/plugin/request/prepare.ts +47 -6
- package/src/plugin/request-helpers/thinking.ts +8 -4
- package/src/plugin/request-helpers.test.ts +22 -1
- package/src/plugin/request.test.ts +80 -0
- package/src/plugin/user-agent.test.ts +50 -0
- package/src/plugin/user-agent.ts +76 -0
- package/src/plugin.ts +31 -0
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.6",
|
|
5
5
|
"author": "jenslys",
|
|
6
6
|
"repository": "https://github.com/jenslys/opencode-gemini-auth",
|
|
7
7
|
"files": [
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
"license": "MIT",
|
|
12
12
|
"type": "module",
|
|
13
13
|
"scripts": {
|
|
14
|
-
"update:gemini-cli": "git -C .local/gemini-cli pull --ff-only"
|
|
14
|
+
"update:gemini-cli": "git -C .local/gemini-cli pull --ff-only",
|
|
15
|
+
"update:gemini-cli-version": "node commands/sync-gemini-cli-version.mjs",
|
|
16
|
+
"update:gemini-cli-sync": "npm run update:gemini-cli && npm run update:gemini-cli-version"
|
|
15
17
|
},
|
|
16
18
|
"devDependencies": {
|
|
17
19
|
"@types/bun": "latest"
|
package/src/constants.ts
CHANGED
|
@@ -28,7 +28,6 @@ export const GEMINI_REDIRECT_URI = "http://localhost:8085/oauth2callback";
|
|
|
28
28
|
export const GEMINI_CODE_ASSIST_ENDPOINT = "https://cloudcode-pa.googleapis.com";
|
|
29
29
|
|
|
30
30
|
export const CODE_ASSIST_HEADERS = {
|
|
31
|
-
"User-Agent": "google-api-nodejs-client/9.15.1",
|
|
32
31
|
"X-Goog-Api-Client": "gl-node/22.17.0",
|
|
33
32
|
"Client-Metadata": "ideType=IDE_UNSPECIFIED,platform=PLATFORM_UNSPECIFIED,pluginType=GEMINI",
|
|
34
33
|
} as const;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CODE_ASSIST_HEADERS, GEMINI_CODE_ASSIST_ENDPOINT } from "../../constants";
|
|
2
2
|
import { logGeminiDebugResponse, startGeminiDebugRequest } from "../debug";
|
|
3
|
+
import { buildGeminiCliUserAgent } from "../user-agent";
|
|
3
4
|
import {
|
|
4
5
|
FREE_TIER_ID,
|
|
5
6
|
type LoadCodeAssistPayload,
|
|
@@ -27,6 +28,7 @@ export async function loadManagedProject(
|
|
|
27
28
|
const headers = {
|
|
28
29
|
"Content-Type": "application/json",
|
|
29
30
|
Authorization: `Bearer ${accessToken}`,
|
|
31
|
+
"User-Agent": buildGeminiCliUserAgent(),
|
|
30
32
|
...CODE_ASSIST_HEADERS,
|
|
31
33
|
};
|
|
32
34
|
const debugContext = startGeminiDebugRequest({
|
|
@@ -92,6 +94,7 @@ export async function onboardManagedProject(
|
|
|
92
94
|
const headers = {
|
|
93
95
|
"Content-Type": "application/json",
|
|
94
96
|
Authorization: `Bearer ${accessToken}`,
|
|
97
|
+
"User-Agent": buildGeminiCliUserAgent(),
|
|
95
98
|
...CODE_ASSIST_HEADERS,
|
|
96
99
|
};
|
|
97
100
|
|
|
@@ -143,6 +146,7 @@ export async function retrieveUserQuota(
|
|
|
143
146
|
const headers = {
|
|
144
147
|
"Content-Type": "application/json",
|
|
145
148
|
Authorization: `Bearer ${accessToken}`,
|
|
149
|
+
"User-Agent": buildGeminiCliUserAgent(),
|
|
146
150
|
...CODE_ASSIST_HEADERS,
|
|
147
151
|
};
|
|
148
152
|
|
|
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
|
|
|
2
2
|
|
|
3
3
|
import { CODE_ASSIST_HEADERS, GEMINI_CODE_ASSIST_ENDPOINT } from "../../constants";
|
|
4
4
|
import { normalizeThinkingConfig } from "../request-helpers";
|
|
5
|
+
import { buildGeminiCliUserAgent } from "../user-agent";
|
|
5
6
|
import { normalizeRequestPayloadIdentifiers, normalizeWrappedIdentifiers } from "./identifiers";
|
|
6
7
|
import { addThoughtSignaturesToFunctionCalls, transformOpenAIToolCalls } from "./openai";
|
|
7
8
|
import { isGenerativeLanguageRequest, toRequestUrlString } from "./shared";
|
|
@@ -11,6 +12,11 @@ const MODEL_FALLBACKS: Record<string, string> = {
|
|
|
11
12
|
"gemini-2.5-flash-image": "gemini-2.5-flash",
|
|
12
13
|
};
|
|
13
14
|
|
|
15
|
+
export interface ThinkingConfigDefaults {
|
|
16
|
+
provider?: unknown;
|
|
17
|
+
models?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
/**
|
|
15
21
|
* Rewrites OpenAI-style requests into Gemini Code Assist request shape.
|
|
16
22
|
*/
|
|
@@ -19,6 +25,7 @@ export function prepareGeminiRequest(
|
|
|
19
25
|
init: RequestInit | undefined,
|
|
20
26
|
accessToken: string,
|
|
21
27
|
projectId: string,
|
|
28
|
+
thinkingConfigDefaults?: ThinkingConfigDefaults,
|
|
22
29
|
): {
|
|
23
30
|
request: RequestInfo;
|
|
24
31
|
init: RequestInit;
|
|
@@ -38,6 +45,7 @@ export function prepareGeminiRequest(
|
|
|
38
45
|
|
|
39
46
|
headers.set("Authorization", `Bearer ${accessToken}`);
|
|
40
47
|
headers.delete("x-api-key");
|
|
48
|
+
headers.delete("x-goog-api-key");
|
|
41
49
|
|
|
42
50
|
const match = toRequestUrlString(input).match(/\/models\/([^:]+):(\w+)/);
|
|
43
51
|
if (!match) {
|
|
@@ -59,7 +67,13 @@ export function prepareGeminiRequest(
|
|
|
59
67
|
let requestIdentifier: string = randomUUID();
|
|
60
68
|
|
|
61
69
|
if (typeof baseInit.body === "string" && baseInit.body) {
|
|
62
|
-
const transformed = transformRequestBody(
|
|
70
|
+
const transformed = transformRequestBody(
|
|
71
|
+
baseInit.body,
|
|
72
|
+
projectId,
|
|
73
|
+
effectiveModel,
|
|
74
|
+
rawModel,
|
|
75
|
+
thinkingConfigDefaults,
|
|
76
|
+
);
|
|
63
77
|
if (transformed.body) {
|
|
64
78
|
body = transformed.body;
|
|
65
79
|
requestIdentifier = transformed.userPromptId;
|
|
@@ -70,7 +84,7 @@ export function prepareGeminiRequest(
|
|
|
70
84
|
headers.set("Accept", "text/event-stream");
|
|
71
85
|
}
|
|
72
86
|
|
|
73
|
-
headers.set("User-Agent",
|
|
87
|
+
headers.set("User-Agent", buildGeminiCliUserAgent(effectiveModel));
|
|
74
88
|
headers.set("X-Goog-Api-Client", CODE_ASSIST_HEADERS["X-Goog-Api-Client"]);
|
|
75
89
|
headers.set("Client-Metadata", CODE_ASSIST_HEADERS["Client-Metadata"]);
|
|
76
90
|
/**
|
|
@@ -95,6 +109,8 @@ function transformRequestBody(
|
|
|
95
109
|
body: string,
|
|
96
110
|
projectId: string,
|
|
97
111
|
effectiveModel: string,
|
|
112
|
+
requestedModel: string,
|
|
113
|
+
thinkingConfigDefaults?: ThinkingConfigDefaults,
|
|
98
114
|
): { body?: string; userPromptId: string } {
|
|
99
115
|
const fallbackId = randomUUID();
|
|
100
116
|
try {
|
|
@@ -113,7 +129,11 @@ function transformRequestBody(
|
|
|
113
129
|
const requestPayload = { ...parsedBody };
|
|
114
130
|
transformOpenAIToolCalls(requestPayload);
|
|
115
131
|
addThoughtSignaturesToFunctionCalls(requestPayload);
|
|
116
|
-
normalizeThinking(
|
|
132
|
+
normalizeThinking(
|
|
133
|
+
requestPayload,
|
|
134
|
+
resolveDefaultThinkingConfig(thinkingConfigDefaults, requestedModel, effectiveModel),
|
|
135
|
+
thinkingConfigDefaults?.provider,
|
|
136
|
+
);
|
|
117
137
|
normalizeSystemInstruction(requestPayload);
|
|
118
138
|
normalizeCachedContent(requestPayload);
|
|
119
139
|
stripThoughtPartsFromHistory(requestPayload);
|
|
@@ -137,9 +157,30 @@ function transformRequestBody(
|
|
|
137
157
|
}
|
|
138
158
|
}
|
|
139
159
|
|
|
140
|
-
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 {
|
|
141
177
|
const rawGenerationConfig = requestPayload.generationConfig as Record<string, unknown> | undefined;
|
|
142
|
-
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);
|
|
143
184
|
if (normalizedThinking) {
|
|
144
185
|
if (rawGenerationConfig) {
|
|
145
186
|
rawGenerationConfig.thinkingConfig = normalizedThinking;
|
|
@@ -150,7 +191,7 @@ function normalizeThinking(requestPayload: Record<string, unknown>): void {
|
|
|
150
191
|
return;
|
|
151
192
|
}
|
|
152
193
|
|
|
153
|
-
if (rawGenerationConfig
|
|
194
|
+
if (hasRequestThinkingConfig && rawGenerationConfig) {
|
|
154
195
|
delete rawGenerationConfig.thinkingConfig;
|
|
155
196
|
requestPayload.generationConfig = rawGenerationConfig;
|
|
156
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
|
+
});
|
|
@@ -25,6 +25,7 @@ describe("request helpers", () => {
|
|
|
25
25
|
headers: {
|
|
26
26
|
"Content-Type": "application/json",
|
|
27
27
|
"x-api-key": "should-be-removed",
|
|
28
|
+
"x-goog-api-key": "should-also-be-removed",
|
|
28
29
|
},
|
|
29
30
|
body: JSON.stringify({
|
|
30
31
|
contents: [{ role: "user", parts: [{ text: "hi" }] }],
|
|
@@ -43,6 +44,9 @@ describe("request helpers", () => {
|
|
|
43
44
|
const headers = new Headers(result.init.headers);
|
|
44
45
|
expect(headers.get("Authorization")).toBe("Bearer token-123");
|
|
45
46
|
expect(headers.get("x-api-key")).toBeNull();
|
|
47
|
+
expect(headers.get("x-goog-api-key")).toBeNull();
|
|
48
|
+
expect(headers.get("User-Agent")).toContain("GeminiCLI/");
|
|
49
|
+
expect(headers.get("User-Agent")).toContain("/gemini-3-flash-preview ");
|
|
46
50
|
expect(headers.get("Accept")).toBe("text/event-stream");
|
|
47
51
|
expect(headers.get("x-activity-request-id")).toBeTruthy();
|
|
48
52
|
|
|
@@ -118,4 +122,80 @@ describe("request helpers", () => {
|
|
|
118
122
|
expect(payload).toContain('"responseId":"trace-456"');
|
|
119
123
|
expect(payload).not.toContain('"traceId"');
|
|
120
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
|
+
});
|
|
121
201
|
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { GEMINI_CLI_VERSION } from "./gemini-cli-version";
|
|
4
|
+
import { buildGeminiCliUserAgent, getGeminiCliVersion, userAgentInternals } from "./user-agent";
|
|
5
|
+
|
|
6
|
+
const originalNpmPackageVersion = process.env.npm_package_version;
|
|
7
|
+
const originalExplicitVersion = process.env.OPENCODE_GEMINI_CLI_VERSION;
|
|
8
|
+
|
|
9
|
+
describe("user-agent", () => {
|
|
10
|
+
afterEach(() => {
|
|
11
|
+
if (originalNpmPackageVersion === undefined) {
|
|
12
|
+
delete process.env.npm_package_version;
|
|
13
|
+
} else {
|
|
14
|
+
process.env.npm_package_version = originalNpmPackageVersion;
|
|
15
|
+
}
|
|
16
|
+
if (originalExplicitVersion === undefined) {
|
|
17
|
+
delete process.env.OPENCODE_GEMINI_CLI_VERSION;
|
|
18
|
+
} else {
|
|
19
|
+
process.env.OPENCODE_GEMINI_CLI_VERSION = originalExplicitVersion;
|
|
20
|
+
}
|
|
21
|
+
userAgentInternals.resetCache();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("prefers OPENCODE_GEMINI_CLI_VERSION when available", () => {
|
|
25
|
+
process.env.OPENCODE_GEMINI_CLI_VERSION = "8.8.8-explicit";
|
|
26
|
+
process.env.npm_package_version = "9.9.9-test";
|
|
27
|
+
userAgentInternals.resetCache();
|
|
28
|
+
|
|
29
|
+
expect(getGeminiCliVersion()).toBe("8.8.8-explicit");
|
|
30
|
+
expect(buildGeminiCliUserAgent("gemini-3-flash-preview")).toContain("/8.8.8-explicit/");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("prefers synced GEMINI_CLI_VERSION over npm_package_version", () => {
|
|
34
|
+
delete process.env.OPENCODE_GEMINI_CLI_VERSION;
|
|
35
|
+
process.env.npm_package_version = "9.9.9-test";
|
|
36
|
+
userAgentInternals.resetCache();
|
|
37
|
+
|
|
38
|
+
expect(getGeminiCliVersion()).toBe(GEMINI_CLI_VERSION);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it("builds a GeminiCLI-style user agent", () => {
|
|
42
|
+
delete process.env.OPENCODE_GEMINI_CLI_VERSION;
|
|
43
|
+
delete process.env.npm_package_version;
|
|
44
|
+
userAgentInternals.resetCache();
|
|
45
|
+
|
|
46
|
+
const userAgent = buildGeminiCliUserAgent("gemini-3-flash-preview");
|
|
47
|
+
expect(userAgent).toContain("GeminiCLI/");
|
|
48
|
+
expect(userAgent).toContain(`/gemini-3-flash-preview `);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { GEMINI_CLI_VERSION } from "./gemini-cli-version";
|
|
5
|
+
|
|
6
|
+
const GEMINI_CLI_UA_NAME = "GeminiCLI";
|
|
7
|
+
const GEMINI_CLI_DEFAULT_MODEL = "gemini-code-assist";
|
|
8
|
+
|
|
9
|
+
let cachedGeminiCliVersion: string | undefined;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolves plugin version for User-Agent:
|
|
13
|
+
* 1) explicit override (`OPENCODE_GEMINI_CLI_VERSION`)
|
|
14
|
+
* 2) synced Gemini CLI version file (`src/plugin/gemini-cli-version.ts`)
|
|
15
|
+
* 3) package-manager runtime env (`npm_package_version`)
|
|
16
|
+
* 4) local package.json next to the plugin sources
|
|
17
|
+
* 5) cwd package.json as final fallback
|
|
18
|
+
*/
|
|
19
|
+
export function getGeminiCliVersion(): string {
|
|
20
|
+
if (cachedGeminiCliVersion) {
|
|
21
|
+
return cachedGeminiCliVersion;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const explicitVersion = process.env.OPENCODE_GEMINI_CLI_VERSION?.trim();
|
|
25
|
+
if (explicitVersion) {
|
|
26
|
+
cachedGeminiCliVersion = explicitVersion;
|
|
27
|
+
return cachedGeminiCliVersion;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (GEMINI_CLI_VERSION.trim()) {
|
|
31
|
+
cachedGeminiCliVersion = GEMINI_CLI_VERSION.trim();
|
|
32
|
+
return cachedGeminiCliVersion;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const envVersion = process.env.npm_package_version?.trim();
|
|
36
|
+
if (envVersion) {
|
|
37
|
+
cachedGeminiCliVersion = envVersion;
|
|
38
|
+
return cachedGeminiCliVersion;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
const candidatePaths = [
|
|
43
|
+
join(moduleDir, "../../package.json"),
|
|
44
|
+
join(moduleDir, "../package.json"),
|
|
45
|
+
join(process.cwd(), "package.json"),
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
for (const packagePath of candidatePaths) {
|
|
49
|
+
try {
|
|
50
|
+
const parsed = JSON.parse(readFileSync(packagePath, "utf8")) as { version?: unknown };
|
|
51
|
+
if (typeof parsed.version === "string" && parsed.version.trim()) {
|
|
52
|
+
cachedGeminiCliVersion = parsed.version.trim();
|
|
53
|
+
return cachedGeminiCliVersion;
|
|
54
|
+
}
|
|
55
|
+
} catch {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
cachedGeminiCliVersion = "0.0.0";
|
|
61
|
+
return cachedGeminiCliVersion;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Builds a Gemini CLI-style User-Agent string.
|
|
66
|
+
*/
|
|
67
|
+
export function buildGeminiCliUserAgent(model?: string): string {
|
|
68
|
+
const modelSegment = model?.trim() || GEMINI_CLI_DEFAULT_MODEL;
|
|
69
|
+
return `${GEMINI_CLI_UA_NAME}/${getGeminiCliVersion()}/${modelSegment} (${process.platform}; ${process.arch})`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export const userAgentInternals = {
|
|
73
|
+
resetCache() {
|
|
74
|
+
cachedGeminiCliVersion = undefined;
|
|
75
|
+
},
|
|
76
|
+
};
|
package/src/plugin.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { maybeShowGeminiCapacityToast, maybeShowGeminiTestToast } from "./plugin
|
|
|
12
12
|
import {
|
|
13
13
|
isGenerativeLanguageRequest,
|
|
14
14
|
prepareGeminiRequest,
|
|
15
|
+
type ThinkingConfigDefaults,
|
|
15
16
|
transformGeminiResponse,
|
|
16
17
|
} from "./plugin/request";
|
|
17
18
|
import { fetchWithRetry } from "./plugin/retry";
|
|
@@ -68,6 +69,7 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
68
69
|
const configuredProjectId = resolveConfiguredProjectId(provider);
|
|
69
70
|
latestGeminiConfiguredProjectId = configuredProjectId;
|
|
70
71
|
normalizeProviderModelCosts(provider);
|
|
72
|
+
const thinkingConfigDefaults = resolveThinkingConfigDefaults(provider);
|
|
71
73
|
|
|
72
74
|
return {
|
|
73
75
|
apiKey: "",
|
|
@@ -109,6 +111,7 @@ export const GeminiCLIOAuthPlugin = async (
|
|
|
109
111
|
init,
|
|
110
112
|
authRecord.access,
|
|
111
113
|
projectContext.effectiveProjectId,
|
|
114
|
+
thinkingConfigDefaults,
|
|
112
115
|
);
|
|
113
116
|
const debugContext = startGeminiDebugRequest({
|
|
114
117
|
originalUrl: toUrlString(input),
|
|
@@ -187,6 +190,34 @@ function normalizeProviderModelCosts(provider: Provider): void {
|
|
|
187
190
|
}
|
|
188
191
|
}
|
|
189
192
|
|
|
193
|
+
function resolveThinkingConfigDefaults(provider: Provider): ThinkingConfigDefaults | undefined {
|
|
194
|
+
const providerOptions =
|
|
195
|
+
provider && typeof provider === "object"
|
|
196
|
+
? ((provider as { options?: Record<string, unknown> }).options ?? undefined)
|
|
197
|
+
: undefined;
|
|
198
|
+
const providerThinkingConfig = providerOptions?.thinkingConfig;
|
|
199
|
+
|
|
200
|
+
const modelThinkingConfigByModel: Record<string, unknown> = {};
|
|
201
|
+
for (const [modelId, model] of Object.entries(provider.models ?? {})) {
|
|
202
|
+
if (!model || typeof model !== "object") {
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
const modelOptions = (model as { options?: Record<string, unknown> }).options;
|
|
206
|
+
if (modelOptions && typeof modelOptions === "object" && "thinkingConfig" in modelOptions) {
|
|
207
|
+
modelThinkingConfigByModel[modelId] = modelOptions.thinkingConfig;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (providerThinkingConfig === undefined && Object.keys(modelThinkingConfigByModel).length === 0) {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
provider: providerThinkingConfig,
|
|
217
|
+
models: modelThinkingConfigByModel,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
190
221
|
async function ensureProjectContextOrThrow(
|
|
191
222
|
authRecord: OAuthAuthDetails,
|
|
192
223
|
client: PluginClient,
|