Skip to content
Merged
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
29 changes: 21 additions & 8 deletions open-sse/executors/gemini-cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,12 @@ export class GeminiCLIExecutor extends BaseExecutor {
return `${this.config.baseUrl}:${action}`;
}

buildHeaders(credentials, stream = true, clientHeaders?: Record<string, string> | null, model?: string) {
buildHeaders(
credentials,
stream = true,
clientHeaders?: Record<string, string> | null,
model?: string
) {
void clientHeaders;
const raw = getGeminiCliHeaders(
normalizeGeminiModel(model || "unknown"),
Expand Down Expand Up @@ -264,7 +269,12 @@ export class GeminiCLIExecutor extends BaseExecutor {
console.warn(
"[OmniRoute] loadCodeAssist returned no project — attempting managed project onboarding"
);
projectId = await this.onboardManagedProject(accessToken, extractDefaultTierId(data), {}, currentModel);
projectId = await this.onboardManagedProject(
accessToken,
extractDefaultTierId(data),
{},
currentModel
);
}

if (!projectId) {
Expand All @@ -287,9 +297,7 @@ export class GeminiCLIExecutor extends BaseExecutor {
async transformRequest(model, body, stream, credentials) {
const currentModel = normalizeGeminiModel(model);
const normalizedBody =
shouldStripCloudCodeThinking(this.provider, currentModel) &&
body &&
typeof body === "object"
shouldStripCloudCodeThinking(this.provider, currentModel) && body && typeof body === "object"
? stripCloudCodeThinkingConfig(body)
: body;

Expand All @@ -304,7 +312,11 @@ export class GeminiCLIExecutor extends BaseExecutor {

const envelope: Record<string, any> = {
model: currentModel,
project: bodyRecord.project || credentials.projectId || "",
project:
bodyRecord.project ||
credentials.projectId ||
(credentials.providerSpecificData as Record<string, unknown>)?.projectId ||
"",
user_prompt_id: bodyRecord.user_prompt_id || generateGeminiCliRequestId(),
request: {
...requestRecord,
Expand All @@ -318,8 +330,9 @@ export class GeminiCLIExecutor extends BaseExecutor {
}
}

// Refresh the project ID via loadCodeAssist (cached for 30s).
if (credentials.accessToken) {
// Refresh the project ID via loadCodeAssist (cached for 30s) only when project not provided
// and credentials have an access token
if (!envelope.project && credentials.accessToken) {
const freshProject = await this.refreshProject(credentials.accessToken, currentModel);
if (freshProject) {
envelope.project = freshProject;
Expand Down
22 changes: 22 additions & 0 deletions src/app/(dashboard)/dashboard/providers/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6012,6 +6012,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
codexOpenaiStoreEnabled: false,
consoleApiKey: "",
ccCompatibleContext1m: false,
geminiProjectId: "",
blockExtraUsage:
connection?.provider === "claude"
? isClaudeExtraUsageBlockEnabled(connection?.provider, connection?.providerSpecificData)
Expand All @@ -6037,6 +6038,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
const isCloudflare = connection?.provider === "cloudflare-ai";
const isCodex = connection?.provider === "codex";
const isClaude = connection?.provider === "claude";
const isGeminiCli = connection?.provider === "gemini-cli";
const localProviderMetadata = getLocalProviderMetadata(connection?.provider);
const isLocalSelfHostedProvider = !!localProviderMetadata;
const isSearxng = connection?.provider === "searxng-search";
Expand Down Expand Up @@ -6099,6 +6101,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
codexOpenaiStoreEnabled: connection.providerSpecificData?.openaiStoreEnabled === true,
consoleApiKey: existingConsoleApiKey,
ccCompatibleContext1m: ccRequestDefaults.context1m,
geminiProjectId: (connection.providerSpecificData?.projectId as string) || "",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The initialization of geminiProjectId only checks providerSpecificData. Since projectId is now a top-level field in the database (as seen in the API and schema changes), it should be prioritized during initialization to ensure values are correctly loaded from the new field.

blockExtraUsage: isClaudeExtraUsageBlockEnabled(
connection.provider,
connection.providerSpecificData
Expand Down Expand Up @@ -6205,6 +6208,10 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
healthCheckInterval: formData.healthCheckInterval,
};

if (isGeminiCli) {
updates.projectId = formData.geminiProjectId.trim() || null;
}

if (isGooglePse && !formData.cx.trim()) {
setSaveError(t("searchEngineIdRequired"));
return;
Expand Down Expand Up @@ -6326,6 +6333,9 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
updates.providerSpecificData.openaiStoreEnabled =
formData.codexOpenaiStoreEnabled === true;
}
if (isGeminiCli) {
updates.providerSpecificData.projectId = formData.geminiProjectId.trim() || undefined;
}
Comment on lines +6336 to +6338

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Storing projectId inside providerSpecificData is redundant because it is already saved as a top-level field at line 6212. Furthermore, using undefined here prevents clearing the value if it already exists in providerSpecificData due to the spread-merge logic in the API route (src/app/api/providers/[id]/route.ts:164). It is recommended to remove this redundant assignment to keep the database clean.

}
const error = (await onSave(updates)) as void | unknown;
if (error) {
Expand Down Expand Up @@ -6420,6 +6430,18 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
/>
</div>
)}
{isGeminiCli && (
<div className="flex flex-col gap-4 rounded-lg border border-border/50 bg-surface/20 p-4">
<Input
label={t("geminiCliProjectIdLabel")}
value={formData.geminiProjectId}
onChange={(e) => setFormData({ ...formData, geminiProjectId: e.target.value })}
placeholder={t("geminiCliProjectIdPlaceholder")}
hint={t("geminiCliProjectIdHint")}
className="font-mono text-xs"
/>
</div>
)}
{isOAuth && connection.email && (
<div className="bg-sidebar/50 p-3 rounded-lg">
<p className="text-sm text-text-muted mb-1">{t("email")}</p>
Expand Down
2 changes: 2 additions & 0 deletions src/app/api/providers/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
healthCheckInterval,
group,
maxConcurrent,
projectId,
providerSpecificData: incomingPsd,
} = body;

Expand All @@ -152,6 +153,7 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
if (healthCheckInterval !== undefined) updateData.healthCheckInterval = healthCheckInterval;
if (group !== undefined) updateData.group = group;
if (maxConcurrent !== undefined) updateData.maxConcurrent = maxConcurrent;
if (projectId !== undefined) updateData.projectId = projectId;

// Merge providerSpecificData (partial update — preserve existing keys not sent by caller)
if (incomingPsd !== undefined && incomingPsd !== null && typeof incomingPsd === "object") {
Expand Down
3 changes: 3 additions & 0 deletions src/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -3011,6 +3011,9 @@
"extraApiKeysHint": "Extra Api Keys Hint",
"extraApiKeysLabel": "Extra Api Keys Label",
"googlePseInfo": "Google Pse Info",
"geminiCliProjectIdHint": "Your Google Cloud Project ID. Required for accounts with exceptions. Enter your GCP Project ID to use with Gemini CLI.",
"geminiCliProjectIdLabel": "Google Cloud Project ID",
"geminiCliProjectIdPlaceholder": "my-gcp-project-id",
"grokWebCookieHint": "Grok Web Cookie Hint",
"grokWebCookiePlaceholder": "Grok Web Cookie Placeholder",
"herokuBaseUrlHint": "Heroku Base Url Hint",
Expand Down
1 change: 1 addition & 0 deletions src/shared/validation/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,7 @@ export const updateProviderConnectionSchema = z
healthCheckInterval: z.coerce.number().int().min(0).optional(),
group: z.union([z.string().max(100), z.null()]).optional(),
maxConcurrent: z.union([z.null(), z.coerce.number().int().min(0)]).optional(),
projectId: z.union([z.string(), z.null()]).optional(),
// Partial patch of per-connection provider-specific settings (e.g. quota toggles)
providerSpecificData: z
.record(z.string(), z.unknown())
Expand Down
19 changes: 8 additions & 11 deletions tests/unit/executor-gemini-cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ test("GeminiCLIExecutor.buildHeaders derives the User-Agent from the request mod
assert.notEqual(flashHeaders["User-Agent"], proHeaders["User-Agent"]);
});

test("GeminiCLIExecutor.refreshProject caches loadCodeAssist lookups and transformRequest updates body.project", async () => {
test("GeminiCLIExecutor.refreshProject caches loadCodeAssist lookups and transformRequest preserves existing body.project", async () => {
const executor = new GeminiCLIExecutor();
const originalFetch = globalThis.fetch;
let calls = 0;
Expand All @@ -107,7 +107,7 @@ test("GeminiCLIExecutor.refreshProject caches loadCodeAssist lookups and transfo
assert.equal(first, "fresh-project-id");
assert.equal(second, "fresh-project-id");
assert.equal(calls, 1);
assert.equal(transformed.project, "fresh-project-id");
assert.equal(transformed.project, "stale-project");
} finally {
globalThis.fetch = originalFetch;
}
Expand Down Expand Up @@ -337,10 +337,10 @@ test("GeminiCLIExecutor.execute applies CLI fingerprint to the final Cloud Code
});
}

return new Response(
'data: {"candidates":[{"content":{"parts":[{"text":"ok"}]}}]}\n\n',
{ status: 200, headers: { "Content-Type": "text/event-stream" } }
);
return new Response('data: {"candidates":[{"content":{"parts":[{"text":"ok"}]}}]}\n\n', {
status: 200,
headers: { "Content-Type": "text/event-stream" },
});
};

try {
Expand Down Expand Up @@ -368,13 +368,10 @@ test("GeminiCLIExecutor.execute applies CLI fingerprint to the final Cloud Code
"Authorization",
]);
assert.equal(finalBody.model, "gemini-3.1-pro-preview");
assert.equal(finalBody.project, "project-live");
assert.equal(finalBody.project, "old-project");
assert.match(finalBody.user_prompt_id, /^agent-/);
assert.match(finalBody.request.session_id, /^-\d+$/);
assert.match(
finalCall.headers["User-Agent"],
/^GeminiCLI\/0\.40\.1\/gemini-3\.1-pro-preview /
);
assert.match(finalCall.headers["User-Agent"], /^GeminiCLI\/0\.40\.1\/gemini-3\.1-pro-preview /);
assert.equal(finalCall.headers.Accept, "*/*");
} finally {
setCliCompatProviders([]);
Expand Down