mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-06 15:22:12 +03:00
feat(gemini-cli): add custom projectId support (UI, DB, executor) (#1991)
Integrated into release/v3.8.0
This commit is contained in:
@@ -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"),
|
||||
@@ -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) {
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
@@ -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;
|
||||
|
||||
@@ -5995,6 +5995,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
codexOpenaiStoreEnabled: false,
|
||||
consoleApiKey: "",
|
||||
ccCompatibleContext1m: false,
|
||||
geminiProjectId: "",
|
||||
blockExtraUsage:
|
||||
connection?.provider === "claude"
|
||||
? isClaudeExtraUsageBlockEnabled(connection?.provider, connection?.providerSpecificData)
|
||||
@@ -6020,6 +6021,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";
|
||||
@@ -6082,6 +6084,7 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
codexOpenaiStoreEnabled: connection.providerSpecificData?.openaiStoreEnabled === true,
|
||||
consoleApiKey: existingConsoleApiKey,
|
||||
ccCompatibleContext1m: ccRequestDefaults.context1m,
|
||||
geminiProjectId: (connection.providerSpecificData?.projectId as string) || "",
|
||||
blockExtraUsage: isClaudeExtraUsageBlockEnabled(
|
||||
connection.provider,
|
||||
connection.providerSpecificData
|
||||
@@ -6188,6 +6191,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;
|
||||
@@ -6309,6 +6316,9 @@ function EditConnectionModal({ isOpen, connection, onSave, onClose }: EditConnec
|
||||
updates.providerSpecificData.openaiStoreEnabled =
|
||||
formData.codexOpenaiStoreEnabled === true;
|
||||
}
|
||||
if (isGeminiCli) {
|
||||
updates.providerSpecificData.projectId = formData.geminiProjectId.trim() || undefined;
|
||||
}
|
||||
}
|
||||
const error = (await onSave(updates)) as void | unknown;
|
||||
if (error) {
|
||||
@@ -6403,6 +6413,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>
|
||||
|
||||
@@ -126,6 +126,7 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||
healthCheckInterval,
|
||||
group,
|
||||
maxConcurrent,
|
||||
projectId,
|
||||
providerSpecificData: incomingPsd,
|
||||
} = body;
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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([]);
|
||||
|
||||
Reference in New Issue
Block a user