forked from boos/BoosAPI
fix(proxy): honor account proxy during oauth discovery
This commit is contained in:
@@ -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'));
|
||||
|
||||
@@ -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(() => '');
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user