fix(desktop): route model requests through configured proxy
diff --git a/apps/desktop/src/main/__tests__/subscription-model-fetch.test.ts b/apps/desktop/src/main/__tests__/subscription-model-fetch.test.ts new file mode 100644 index 0000000..ef7d5e5 --- /dev/null +++ b/apps/desktop/src/main/__tests__/subscription-model-fetch.test.ts
@@ -0,0 +1,71 @@ +import assert from 'node:assert/strict'; +import { describe, test } from 'node:test'; +import { FETCH_PROXY_SNAPSHOT, proxiedFetch, setActiveProxy } from '@maka/runtime'; +import { PROXY_DEFAULTS } from '@maka/core/settings/network-settings'; +import { createSubscriptionModelFetch } from '../subscription-model-fetch.js'; + +describe('desktop model fetch', () => { + test('provider requests use the active Maka proxy transport', () => { + const buildModelFetch = createSubscriptionModelFetch({ + claudeSubscription: {} as never, + openAiCodex: {} as never, + xaiOAuth: {} as never, + }); + + const modelFetch = buildModelFetch( + { + slug: 'openai-test', + name: 'OpenAI test', + providerType: 'openai', + baseUrl: 'https://api.openai.com/v1', + defaultModel: 'gpt-test', + enabled: true, + createdAt: 1, + updatedAt: 1, + }, + 'session-test', + 'gpt-test', + ); + + assert.equal(modelFetch, proxiedFetch); + setActiveProxy({ + ...PROXY_DEFAULTS, + enabled: true, + host: '127.0.0.1', + port: 7890, + }); + try { + const snapshot = ( + modelFetch as typeof fetch & { + [FETCH_PROXY_SNAPSHOT]?: { host?: string; port?: number }; + } + )[FETCH_PROXY_SNAPSHOT]; + assert.equal(snapshot?.host, '127.0.0.1'); + assert.equal(snapshot?.port, 7890); + + const subscriptionFetch = buildModelFetch( + { + slug: 'codex-test', + name: 'Codex test', + providerType: 'openai-codex', + baseUrl: 'https://chatgpt.com/backend-api/codex', + defaultModel: 'gpt-test', + enabled: true, + createdAt: 1, + updatedAt: 1, + }, + 'session-test', + 'gpt-test', + ); + const subscriptionSnapshot = ( + subscriptionFetch as typeof fetch & { + [FETCH_PROXY_SNAPSHOT]?: { host?: string; port?: number }; + } + )[FETCH_PROXY_SNAPSHOT]; + assert.equal(subscriptionSnapshot?.host, '127.0.0.1'); + assert.equal(subscriptionSnapshot?.port, 7890); + } finally { + setActiveProxy(null); + } + }); +});
diff --git a/apps/desktop/src/main/subscription-model-fetch.ts b/apps/desktop/src/main/subscription-model-fetch.ts index fbe14a5..40e7b54 100644 --- a/apps/desktop/src/main/subscription-model-fetch.ts +++ b/apps/desktop/src/main/subscription-model-fetch.ts
@@ -1,5 +1,9 @@ import type { LlmConnection } from '@maka/core/llm-connections'; -import { buildSubscriptionModelFetch as buildRuntimeSubscriptionModelFetch } from '@maka/runtime'; +import { + buildSubscriptionModelFetch as buildRuntimeSubscriptionModelFetch, + inheritFetchProxySnapshot, + proxiedFetch, +} from '@maka/runtime'; import { type ClaudeSubscriptionService, isCloakEnabled, @@ -18,19 +22,29 @@ connection: LlmConnection, sessionId: string, modelId: string, - ): typeof fetch | undefined { + ): typeof fetch { if (connection.providerType === 'claude-subscription' && isCloakEnabled()) { - return buildClaudeSubscriptionCloakedFetch(connection, deps.claudeSubscription, sessionId, modelId); + return inheritFetchProxySnapshot( + buildClaudeSubscriptionCloakedFetch( + connection, + deps.claudeSubscription, + sessionId, + modelId, + proxiedFetch, + ), + proxiedFetch, + ); } if ( connection.providerType === 'openai-codex' || connection.providerType === 'github-copilot' || connection.providerType === 'xai-oauth' ) { - return buildRuntimeSubscriptionModelFetch({ + const subscriptionFetch = buildRuntimeSubscriptionModelFetch({ connection, sessionId, modelId, + fetchFn: proxiedFetch, ...(connection.providerType === 'openai-codex' ? { refreshOAuthAccessToken: () => @@ -43,8 +57,11 @@ } : {}), }); + return subscriptionFetch + ? inheritFetchProxySnapshot(subscriptionFetch, proxiedFetch) + : proxiedFetch; } - return undefined; + return proxiedFetch; }; } @@ -53,6 +70,7 @@ claudeSubscription: ClaudeSubscriptionService, sessionId: string, modelId: string, + fetchFn: typeof fetch, ): typeof fetch { return async (url: Parameters<typeof fetch>[0], init?: Parameters<typeof fetch>[1]) => { const [deviceId, accountState] = await Promise.all([ @@ -63,13 +81,13 @@ connection, sessionId, modelId, - fetchFn: fetch, + fetchFn, claude: { cloakEnabled: true, deviceId, accountUuid: accountState.profile?.accountUuid ?? '', }, }); - return (modelFetch ?? fetch)(url, init); + return (modelFetch ?? fetchFn)(url, init); }; }
diff --git a/packages/runtime/src/bots/proxied-fetch.ts b/packages/runtime/src/bots/proxied-fetch.ts index f67496f..8548978 100644 --- a/packages/runtime/src/bots/proxied-fetch.ts +++ b/packages/runtime/src/bots/proxied-fetch.ts
@@ -2,15 +2,24 @@ import { matchesBypassList } from '../network/bypass-matcher.js'; import { buildProxyDispatcher } from '../network/proxy-dispatcher.js'; import { resolveActiveProxy } from '../network/active-proxy-state.js'; +import { FETCH_PROXY_SNAPSHOT } from '../network/scoped-fetch-transport.js'; const DEFAULT_TIMEOUT_MS = 15_000; -export type ProxiedFetchInit = UndiciRequestInit & { - signal?: AbortSignal; +export type ProxiedFetchInit = Omit< + NonNullable<Parameters<typeof globalThis.fetch>[1]>, + 'signal' +> & { + signal?: AbortSignal | null; timeoutMs?: number; }; -export async function proxiedFetch(url: string, init: ProxiedFetchInit = {}): Promise<Response> { +export async function proxiedFetch( + input: Parameters<typeof globalThis.fetch>[0], + init: ProxiedFetchInit = {}, +): Promise<Response> { + const url = + typeof input === 'string' ? input : input instanceof URL ? input.toString() : input.url; const proxy = resolveActiveProxy(); let dispatcher: Dispatcher | undefined; if (proxy && !matchesBypassList(new URL(url).hostname, proxy.bypassList)) { @@ -52,7 +61,10 @@ }) : undefined; - const request = fetch(url, { ...fetchInit, dispatcher, signal: requestSignal }).catch((error) => { + const request = fetch( + input as Parameters<typeof fetch>[0], + { ...fetchInit, dispatcher, signal: requestSignal } as UndiciRequestInit, + ).catch((error) => { if (timedOut) return new Promise<never>(() => {}); throw error; }); @@ -78,3 +90,8 @@ void disposeDispatcher(false); return response; } + +Object.defineProperty(proxiedFetch, FETCH_PROXY_SNAPSHOT, { + get: resolveActiveProxy, + enumerable: false, +});
diff --git a/packages/runtime/src/index.ts b/packages/runtime/src/index.ts index 8044530..a96a249 100644 --- a/packages/runtime/src/index.ts +++ b/packages/runtime/src/index.ts
@@ -1174,6 +1174,8 @@ export { createConnectionEffectFetchTransport, createProxiedFetchTransport, + FETCH_PROXY_SNAPSHOT, + inheritFetchProxySnapshot, } from './network/scoped-fetch-transport.js'; export type { ConnectionEffectFetchTransport,
diff --git a/packages/runtime/src/network/scoped-fetch-transport.ts b/packages/runtime/src/network/scoped-fetch-transport.ts index 9137d46..32c38a5 100644 --- a/packages/runtime/src/network/scoped-fetch-transport.ts +++ b/packages/runtime/src/network/scoped-fetch-transport.ts
@@ -28,6 +28,15 @@ close(): Promise<void>; } +export function inheritFetchProxySnapshot( + fetch: typeof globalThis.fetch, + source: typeof globalThis.fetch, +): typeof globalThis.fetch { + const descriptor = Object.getOwnPropertyDescriptor(source, FETCH_PROXY_SNAPSHOT); + if (descriptor) Object.defineProperty(fetch, FETCH_PROXY_SNAPSHOT, descriptor); + return fetch; +} + export function createConnectionEffectFetchTransport( proxy: ConnectionEffectProxySnapshot | null, ): ConnectionEffectFetchTransport {