fix(proxy): honor account proxy during oauth discovery

This commit is contained in:
ciat-777
2026-03-21 12:46:46 +08:00
parent 4d0c4b8129
commit 04655a8b17
3 changed files with 128 additions and 4 deletions
@@ -7,6 +7,18 @@ import { eq } from 'drizzle-orm';
const getApiTokenMock = vi.fn();
const getModelsMock = vi.fn();
const undiciFetchMock = vi.fn();
const proxyAgentCtorMock = vi.fn();
class MockProxyAgent {
readonly proxyUrl: string;
constructor(proxyUrl: string) {
this.proxyUrl = proxyUrl;
proxyAgentCtorMock(proxyUrl);
}
}
class MockAgent {}
vi.mock('./platforms/index.js', () => ({
getAdapter: () => ({
@@ -17,6 +29,8 @@ vi.mock('./platforms/index.js', () => ({
vi.mock('undici', () => ({
fetch: (...args: unknown[]) => undiciFetchMock(...args),
ProxyAgent: MockProxyAgent,
Agent: MockAgent,
}));
type DbModule = typeof import('../db/index.js');
@@ -47,6 +61,7 @@ describe('refreshModelsForAccount credential discovery', () => {
getApiTokenMock.mockReset();
getModelsMock.mockReset();
undiciFetchMock.mockReset();
proxyAgentCtorMock.mockReset();
await db.delete(schema.routeChannels).run();
await db.delete(schema.tokenRoutes).run();
@@ -595,6 +610,115 @@ describe('refreshModelsForAccount credential discovery', () => {
expect(parsed.runtimeHealth?.state).toBe('unhealthy');
});
it('applies account proxy override to codex oauth cloud discovery requests', async () => {
getApiTokenMock.mockResolvedValue(null);
getModelsMock.mockRejectedValue(new Error('codex oauth discovery should not call adapter.getModels'));
undiciFetchMock.mockResolvedValue({
ok: true,
status: 200,
json: async () => ({
models: [
{ id: 'gpt-5.4' },
],
}),
text: async () => JSON.stringify({ ok: true }),
});
const site = await db.insert(schema.sites).values({
name: 'codex-account-proxy-site',
url: 'https://chatgpt.com/backend-api/codex',
platform: 'codex',
status: 'active',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
username: 'codex-proxy-user@example.com',
accessToken: 'oauth-access-token',
apiToken: null,
status: 'active',
extraConfig: JSON.stringify({
credentialMode: 'session',
proxyUrl: 'http://127.0.0.1:7890',
oauth: {
provider: 'codex',
accountId: 'chatgpt-account-proxy',
email: 'codex-proxy-user@example.com',
planType: 'plus',
},
}),
}).returning().get();
const result = await refreshModelsForAccount(account.id);
expect(result).toMatchObject({
accountId: account.id,
refreshed: true,
status: 'success',
modelCount: 1,
modelsPreview: ['gpt-5.4'],
});
expect(undiciFetchMock).toHaveBeenCalledTimes(1);
expect(proxyAgentCtorMock).toHaveBeenCalledWith('http://127.0.0.1:7890');
expect(undiciFetchMock.mock.calls[0]?.[1]).toMatchObject({
method: 'GET',
dispatcher: expect.any(MockProxyAgent),
});
});
it('applies account proxy override to 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 site = await db.insert(schema.sites).values({
name: 'gemini-account-proxy-site',
url: 'https://gemini.example.com',
platform: 'gemini',
status: 'active',
}).returning().get();
const account = await db.insert(schema.accounts).values({
siteId: site.id,
username: 'gemini-proxy-user@example.com',
accessToken: 'gemini-access-token',
apiToken: null,
status: 'active',
extraConfig: JSON.stringify({
credentialMode: 'session',
proxyUrl: 'http://127.0.0.1:1080',
oauth: {
provider: 'gemini-cli',
projectId: 'project-proxy-demo',
email: 'gemini-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:1080');
expect(String(undiciFetchMock.mock.calls[0]?.[0] || '')).toContain('/projects/project-proxy-demo/services/');
expect(undiciFetchMock.mock.calls[0]?.[1]).toMatchObject({
method: 'GET',
dispatcher: expect.any(MockProxyAgent),
});
});
it('discovers antigravity oauth models via fetchAvailableModels fallback using the oauth project id', async () => {
getApiTokenMock.mockResolvedValue(null);
getModelsMock.mockRejectedValue(new Error('antigravity oauth discovery should not call adapter.getModels'));
+3 -3
View File
@@ -20,7 +20,7 @@ import {
import { invalidateTokenRouterCache } from './tokenRouter.js';
import { setAccountRuntimeHealth } from './accountHealthService.js';
import { clearAllRouteDecisionSnapshots } from './routeDecisionSnapshotStore.js';
import { withAccountProxyOverride, withSiteRecordProxyRequestInit } from './siteProxy.js';
import { withAccountProxyOverride, withExplicitProxyRequestInit, withSiteRecordProxyRequestInit } from './siteProxy.js';
import { getCodexOauthInfoFromExtraConfig, isCodexPlatform } from './oauth/codexAccount.js';
import { buildOauthInfo, getOauthInfoFromExtraConfig } from './oauth/oauthAccount.js';
import { CLAUDE_DEFAULT_ANTHROPIC_VERSION } from './oauth/claudeProvider.js';
@@ -323,7 +323,7 @@ async function validateGeminiCliOauthConnection(input: {
}
const response = await fetch(
`https://serviceusage.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/services/${encodeURIComponent(GEMINI_CLI_REQUIRED_SERVICE)}`,
{
withExplicitProxyRequestInit(getProxyUrlFromExtraConfig(input.account.extraConfig), {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`,
@@ -331,7 +331,7 @@ async function validateGeminiCliOauthConnection(input: {
'User-Agent': GEMINI_CLI_USER_AGENT,
'X-Goog-Api-Client': GEMINI_CLI_GOOGLE_API_CLIENT,
},
},
}, true),
);
if (!response.ok) {
const text = await response.text().catch(() => '');
+1 -1
View File
@@ -469,7 +469,7 @@ export function withSiteRecordProxyRequestInit(
if (mergedHeaders) {
nextOptions.headers = mergedHeaders;
}
const accountNormalized = normalizeSiteProxyUrl(accountProxyUrl);
const accountNormalized = normalizeSiteProxyUrl(accountProxyUrl) ?? accountProxyOverride.getStore();
const siteProxyUrl = resolveProxyUrlForSite(site);
const proxyUrl = accountNormalized || siteProxyUrl;
const isAccountOverride = !!accountNormalized && accountNormalized !== siteProxyUrl;