diff --git a/.github/workflows/custom-action.yml b/.github/workflows/custom-action.yml_ similarity index 100% rename from .github/workflows/custom-action.yml rename to .github/workflows/custom-action.yml_ diff --git a/.gitignore b/.gitignore index 238c624e92..e102aa1da5 100644 --- a/.gitignore +++ b/.gitignore @@ -64,6 +64,10 @@ genaiscript*.tgz *.bak +devproxy/ +devproxy-beta/ +.demo/ +dev-proxy-ca.crt # START Ruler Generated Files .github/copilot-instructions.md diff --git a/package.json b/package.json index be39390450..54e475c257 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,9 @@ "disk:check": "du -h --max-depth=2 | sort -hr | head -n 10", "docs": "cd docs && pnpm run dev", "install:ffmpeg": "sudo apt-get update && sudo apt-get install ffmpeg -y", + "devproxy:install": "sudo bash -c \"$(curl -sL https://aka.ms/devproxy/setup.sh)\"", + "devproxy:start": "cd packages/sample && devproxy", + "devproxy:test": "curl -ikx http://127.0.0.1:8000 https://raw.githubusercontent.com/microsoft/genaiscript/refs/heads/main/SUPPORT.md", "format:check": "turbo format:check", "format:fix": "turbo format:fix", "gcm": "node packages/cli/dist/src/index.js run gcm --model gcm --no-run-trace --no-output-trace", @@ -73,6 +76,7 @@ "retrieval:index": "node packages/cli/dist/src/index.js retrieval index \"packages/sample/src/rag/*\"", "retrieval:search": "node packages/cli/dist/src/index.js retrieval search lorem \"packages/sample/src/rag/*\"", "run:script": "cd packages/sample/ && pnpm run:script", + "run:script:devproxy": "cd packages/sample/ && HTTP_PROXY=http://127.0.0.1:8000 DEBUG=script,genaiscript:fetch* NODE_TLS_REJECT_UNAUTHORIZED=0 pnpm run:script", "serve": "pnpm build:cli && run-p serve:*", "serve:cli": "node --watch --watch-path=packages/cli/dist packages/cli/dist/src/index.js serve --dispatch-progress", "serve:web": "pnpm --filter=@genaiscript/web watch", diff --git a/packages/core/package.json b/packages/core/package.json index 0c64d95e46..92e6ba10a3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -87,6 +87,7 @@ "groq-js": "^1.17.1", "html-escaper": "3.0.3", "html-to-text": "^9.0.5", + "https-proxy-agent": "^7.0.6", "ignore": "^7.0.5", "inflection": "catalog:", "ini": "^5.0.0", diff --git a/packages/core/src/anthropic.ts b/packages/core/src/anthropic.ts index 0a1b995e22..9fbd427d99 100644 --- a/packages/core/src/anthropic.ts +++ b/packages/core/src/anthropic.ts @@ -31,7 +31,7 @@ import { } from "./chattypes.js"; import { logError } from "./util.js"; -import { resolveHttpProxyAgent } from "./proxy.js"; +import { resolveUndiciProxyAgent } from "./proxy.js"; import { ProxyAgent } from "undici"; import { MarkdownTrace } from "./trace.js"; import { createFetch, FetchType } from "./fetch.js"; @@ -303,7 +303,7 @@ const completerFactory = ( // https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching#how-to-implement-prompt-caching const caching = /sonnet|haiku|opus/i.test(model) && req.messages.some((m) => m.cacheControl === "ephemeral"); - const httpAgent = await resolveHttpProxyAgent(); + const httpAgent = await resolveUndiciProxyAgent(); const messagesApi = await resolver(trace, cfg, httpAgent, fetch); dbg("caching", caching); trace?.itemValue(`caching`, caching); diff --git a/packages/core/src/fetch.ts b/packages/core/src/fetch.ts index a7204fdbcf..48a4dae1f1 100644 --- a/packages/core/src/fetch.ts +++ b/packages/core/src/fetch.ts @@ -13,15 +13,15 @@ import { import { errorMessage } from "./error.js"; import { logVerbose } from "./util.js"; import { CancellationOptions } from "./cancellation.js"; -import { resolveHttpProxyAgent } from "./proxy.js"; +import { resolveHttpsProxyAgent } from "./proxy.js"; import { host } from "./host.js"; import { renderWithPrecision } from "./precision.js"; import crossFetch from "cross-fetch"; -import debug from "debug"; import { prettyStrings } from "./pretty.js"; import type { FetchOptions, RetryOptions } from "./types.js"; +import { genaiscriptDebug } from "./debug.js"; -const dbg = debug("genaiscript:fetch"); +const dbg = genaiscriptDebug("fetch"); /** * Parses the retry-after header value. @@ -51,7 +51,7 @@ export function parseRetryAfter(retryAfterHeader: string): number | null { const delaySeconds = Math.max(0, Math.ceil(delayMs / 1000)); return delaySeconds; } - } catch(e) { + } catch (e) { dbg(`failed to parse retry-after header as date: %s`, errorMessage(e)); } } @@ -84,6 +84,7 @@ export type FetchType = ( export async function createFetch( options?: TraceOptions & CancellationOptions & RetryOptions, ): Promise { + options = options || {}; const { retries = FETCH_RETRY_DEFAULT, retryOn = FETCH_RETRY_ON_DEFAULT, @@ -91,24 +92,30 @@ export async function createFetch( retryDelay = FETCH_RETRY_DEFAULT_DEFAULT, maxDelay = FETCH_RETRY_MAX_DELAY_DEFAULT, cancellationToken, - } = options || {}; + } = options; + dbg(`create fetch`); // We create a proxy based on Node.js environment variables. - const agent = await resolveHttpProxyAgent(); + const agent = await resolveHttpsProxyAgent(); // We enrich crossFetch with the proxy. const crossFetchWithProxy: typeof fetch = agent - ? (url, options) => crossFetch(url, { ...(options || {}), dispatcher: agent } as any) + ? (url, options) => crossFetch(url, { ...options, agent } as RequestInit) : crossFetch; + const loggingFetch: typeof fetch = (url, options) => { + dbg(`fetch: %s %s`, options?.method || "GET", url); + return crossFetchWithProxy(url, options); + }; + // Return the default fetch if no retry status codes are specified if (!retryOn?.length) { dbg("no retry logic applied, using crossFetchWithProxy directly"); - return crossFetchWithProxy; + return loggingFetch; } // Create a fetch function with retry logic - const fetchRetry = wrapFetch(crossFetchWithProxy, { + const fetchRetry = wrapFetch(loggingFetch, { retryOn, retries, retryDelay: (attempt, error, response) => { diff --git a/packages/core/src/proxy.ts b/packages/core/src/proxy.ts index c165b28890..9e57467ba9 100644 --- a/packages/core/src/proxy.ts +++ b/packages/core/src/proxy.ts @@ -2,7 +2,18 @@ // Licensed under the MIT License. import { genaiscriptDebug } from "./debug.js"; -const dbg = genaiscriptDebug("proxy"); +const dbg = genaiscriptDebug("fetch:proxy"); + +function resolveProxyUrl() { + const proxy = + process.env.GENAISCRIPT_HTTPS_PROXY || + process.env.GENAISCRIPT_HTTP_PROXY || + process.env.HTTPS_PROXY || + process.env.HTTP_PROXY || + process.env.https_proxy || + process.env.http_proxy; + return proxy; +} /** * Resolves an HTTP proxy agent based on environment variables. @@ -23,19 +34,28 @@ const dbg = genaiscriptDebug("proxy"); * @returns An instance of `HttpsProxyAgent` if a proxy is configured, * or null if no proxy is detected. */ -export async function resolveHttpProxyAgent() { +export async function resolveUndiciProxyAgent() { // We create a proxy based on Node.js environment variables. - const proxy = - process.env.GENAISCRIPT_HTTPS_PROXY || - process.env.GENAISCRIPT_HTTP_PROXY || - process.env.HTTPS_PROXY || - process.env.HTTP_PROXY || - process.env.https_proxy || - process.env.http_proxy; - if (proxy) dbg(`proxy: %s`, proxy); + const proxy = resolveProxyUrl(); if (!proxy) return null; + dbg(`proxy (undici): %s`, proxy); const { ProxyAgent } = await import("undici"); const agent = new ProxyAgent(proxy); + agent.on(`connect`, (info) => dbg(`connect: %s`, info.href)); + agent.on(`connectionError`, (err) => dbg(`connection error: %s`, err.toString())); + agent.on(`disconnect`, () => dbg(`disconnect`)); + return agent; +} + +export async function resolveHttpsProxyAgent() { + const proxyUrl = resolveProxyUrl(); + if (!proxyUrl) return null; + + dbg(`proxy (hpa): %s`, proxyUrl); + const { HttpsProxyAgent } = await import("https-proxy-agent"); + const agent = new HttpsProxyAgent(proxyUrl); + agent.on(`connect`, (info) => dbg(`connect: %s`, info.href)); + agent.on(`error`, (err) => dbg(`error: %s`, err.toString())); return agent; } diff --git a/packages/sample/devproxyrc.json b/packages/sample/devproxyrc.json new file mode 100644 index 0000000000..34c30de255 --- /dev/null +++ b/packages/sample/devproxyrc.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v0.29.0/rc.schema.json", + "plugins": [ + { + "name": "DevToolsPlugin", + "enabled": true, + "pluginPath": "~appFolder/plugins/DevProxy.Plugins.dll", + "configSection": "devTools" + } + ], + "devTools": { + "$schema": "https://raw.githubusercontent.com/dotnet/dev-proxy/main/schemas/v0.29.0/devtoolsplugin.schema.json", + "preferredBrowser": "EdgeDev" + }, + "urlsToWatch": ["*"], + "logLevel": "debug", + "newVersionNotification": "stable", + "showSkipMessages": true, + "asSystemProxy": false +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fb06f2dbe3..ee4769dc2e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -412,6 +412,9 @@ importers: html-to-text: specifier: ^9.0.5 version: 9.0.5 + https-proxy-agent: + specifier: ^7.0.6 + version: 7.0.6 ignore: specifier: ^7.0.5 version: 7.0.5