fix gemini oauth validation site proxy (#298)
This commit is contained in:
@@ -75,7 +75,12 @@ describe('refreshModelsForAccount credential discovery', () => {
|
|||||||
await db.delete(schema.modelAvailability).run();
|
await db.delete(schema.modelAvailability).run();
|
||||||
await db.delete(schema.accountTokens).run();
|
await db.delete(schema.accountTokens).run();
|
||||||
await db.delete(schema.accounts).run();
|
await db.delete(schema.accounts).run();
|
||||||
|
await db.delete(schema.settings).run();
|
||||||
await db.delete(schema.sites).run();
|
await db.delete(schema.sites).run();
|
||||||
|
const { config } = await import('../config.js');
|
||||||
|
config.systemProxyUrl = '';
|
||||||
|
const { invalidateSiteProxyCache } = await import('./siteProxy.js');
|
||||||
|
invalidateSiteProxyCache();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
@@ -874,6 +879,62 @@ describe('refreshModelsForAccount credential discovery', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('inherits site system proxy for gemini oauth validation requests', async () => {
|
||||||
|
getApiTokenMock.mockResolvedValue(null);
|
||||||
|
getModelsMock.mockRejectedValue(new Error('gemini oauth validation should not call adapter.getModels'));
|
||||||
|
undiciFetchMock.mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({
|
||||||
|
state: 'ENABLED',
|
||||||
|
}),
|
||||||
|
text: async () => JSON.stringify({ state: 'ENABLED' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const { config } = await import('../config.js');
|
||||||
|
config.systemProxyUrl = 'http://127.0.0.1:1081';
|
||||||
|
|
||||||
|
const site = await db.insert(schema.sites).values({
|
||||||
|
name: 'gemini-site-proxy-site',
|
||||||
|
url: 'https://cloudcode-pa.googleapis.com',
|
||||||
|
platform: 'gemini-cli',
|
||||||
|
status: 'active',
|
||||||
|
useSystemProxy: true,
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
const account = await db.insert(schema.accounts).values({
|
||||||
|
siteId: site.id,
|
||||||
|
username: 'gemini-site-proxy-user@example.com',
|
||||||
|
accessToken: 'gemini-access-token',
|
||||||
|
apiToken: null,
|
||||||
|
status: 'active',
|
||||||
|
extraConfig: JSON.stringify({
|
||||||
|
credentialMode: 'session',
|
||||||
|
oauth: {
|
||||||
|
provider: 'gemini-cli',
|
||||||
|
projectId: 'project-site-proxy-demo',
|
||||||
|
email: 'gemini-site-proxy-user@example.com',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
const result = await refreshModelsForAccount(account.id);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
accountId: account.id,
|
||||||
|
refreshed: true,
|
||||||
|
status: 'success',
|
||||||
|
modelCount: expect.any(Number),
|
||||||
|
});
|
||||||
|
expect(undiciFetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(proxyAgentCtorMock).toHaveBeenCalledWith('http://127.0.0.1:1081');
|
||||||
|
expect(String(undiciFetchMock.mock.calls[0]?.[0] || '')).toContain('/projects/project-site-proxy-demo/services/');
|
||||||
|
expect(undiciFetchMock.mock.calls[0]?.[1]).toMatchObject({
|
||||||
|
method: 'GET',
|
||||||
|
dispatcher: expect.any(MockProxyAgent),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('refreshes gemini oauth access token during validation through singleflight and reuses the refreshed account state', async () => {
|
it('refreshes gemini oauth access token during validation through singleflight and reuses the refreshed account state', async () => {
|
||||||
getApiTokenMock.mockResolvedValue(null);
|
getApiTokenMock.mockResolvedValue(null);
|
||||||
getModelsMock.mockRejectedValue(new Error('gemini oauth validation should not call adapter.getModels'));
|
getModelsMock.mockRejectedValue(new Error('gemini oauth validation should not call adapter.getModels'));
|
||||||
|
|||||||
@@ -529,7 +529,7 @@ export async function refreshModelsForAccount(
|
|||||||
try {
|
try {
|
||||||
await withTimeout(
|
await withTimeout(
|
||||||
() => withAccountProxyOverride(accountProxyUrl,
|
() => withAccountProxyOverride(accountProxyUrl,
|
||||||
() => validateGeminiCliOauthConnection({ account: discoveryAccount })),
|
() => validateGeminiCliOauthConnection({ site, account: discoveryAccount })),
|
||||||
MODEL_DISCOVERY_TIMEOUT_MS,
|
MODEL_DISCOVERY_TIMEOUT_MS,
|
||||||
`gemini cli oauth validation timeout (${Math.round(MODEL_DISCOVERY_TIMEOUT_MS / 1000)}s)`,
|
`gemini cli oauth validation timeout (${Math.round(MODEL_DISCOVERY_TIMEOUT_MS / 1000)}s)`,
|
||||||
);
|
);
|
||||||
@@ -548,7 +548,7 @@ export async function refreshModelsForAccount(
|
|||||||
};
|
};
|
||||||
await withTimeout(
|
await withTimeout(
|
||||||
() => withAccountProxyOverride(accountProxyUrl,
|
() => withAccountProxyOverride(accountProxyUrl,
|
||||||
() => validateGeminiCliOauthConnection({ account: discoveryAccount })),
|
() => validateGeminiCliOauthConnection({ site, account: discoveryAccount })),
|
||||||
MODEL_DISCOVERY_TIMEOUT_MS,
|
MODEL_DISCOVERY_TIMEOUT_MS,
|
||||||
`gemini cli oauth validation timeout (${Math.round(MODEL_DISCOVERY_TIMEOUT_MS / 1000)}s)`,
|
`gemini cli oauth validation timeout (${Math.round(MODEL_DISCOVERY_TIMEOUT_MS / 1000)}s)`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { fetch } from 'undici';
|
import { fetch } from 'undici';
|
||||||
import { schema } from '../db/index.js';
|
import { schema } from '../db/index.js';
|
||||||
import { getProxyUrlFromExtraConfig } from './accountExtraConfig.js';
|
import { withSiteRecordProxyRequestInit } from './siteProxy.js';
|
||||||
import { withExplicitProxyRequestInit, withSiteRecordProxyRequestInit } from './siteProxy.js';
|
|
||||||
import { getOauthInfoFromAccount } from './oauth/oauthAccount.js';
|
import { getOauthInfoFromAccount } from './oauth/oauthAccount.js';
|
||||||
import { CLAUDE_DEFAULT_ANTHROPIC_VERSION } from './oauth/claudeProvider.js';
|
import { CLAUDE_DEFAULT_ANTHROPIC_VERSION } from './oauth/claudeProvider.js';
|
||||||
import {
|
import {
|
||||||
@@ -175,6 +174,7 @@ export async function discoverClaudeModelsFromCloud(input: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function validateGeminiCliOauthConnection(input: {
|
export async function validateGeminiCliOauthConnection(input: {
|
||||||
|
site: PlatformDiscoverySite;
|
||||||
account: PlatformDiscoveryAccount;
|
account: PlatformDiscoveryAccount;
|
||||||
}): Promise<void> {
|
}): Promise<void> {
|
||||||
const accessToken = (input.account.accessToken || '').trim();
|
const accessToken = (input.account.accessToken || '').trim();
|
||||||
@@ -188,7 +188,7 @@ export async function validateGeminiCliOauthConnection(input: {
|
|||||||
}
|
}
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://serviceusage.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/services/${encodeURIComponent(GEMINI_CLI_REQUIRED_SERVICE)}`,
|
`https://serviceusage.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/services/${encodeURIComponent(GEMINI_CLI_REQUIRED_SERVICE)}`,
|
||||||
withExplicitProxyRequestInit(getProxyUrlFromExtraConfig(input.account.extraConfig), {
|
withSiteRecordProxyRequestInit(input.site, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
@@ -196,7 +196,7 @@ export async function validateGeminiCliOauthConnection(input: {
|
|||||||
'User-Agent': GEMINI_CLI_USER_AGENT,
|
'User-Agent': GEMINI_CLI_USER_AGENT,
|
||||||
'X-Goog-Api-Client': GEMINI_CLI_GOOGLE_API_CLIENT,
|
'X-Goog-Api-Client': GEMINI_CLI_GOOGLE_API_CLIENT,
|
||||||
},
|
},
|
||||||
}, true),
|
}),
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const text = await response.text().catch(() => '');
|
const text = await response.text().catch(() => '');
|
||||||
|
|||||||
Reference in New Issue
Block a user