Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ genaiscript*.tgz

*.bak

devproxy/
devproxy-beta/
.demo/
dev-proxy-ca.crt

# START Ruler Generated Files
.github/copilot-instructions.md
Expand Down
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/anthropic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down
25 changes: 16 additions & 9 deletions packages/core/src/fetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function errorMessage(e) is used, but there is no import or definition for errorMessage in this file. This will cause a runtime error if errorMessage is not defined elsewhere in the scope.

AI-generated content by pr-review-commit undefined_identifier may be incorrect. Use reactions to eval.

}
}
Expand Down Expand Up @@ -84,31 +84,38 @@ export type FetchType = (
export async function createFetch(
options?: TraceOptions & CancellationOptions & RetryOptions,
): Promise<FetchType> {
options = options || {};
const {
retries = FETCH_RETRY_DEFAULT,
retryOn = FETCH_RETRY_ON_DEFAULT,
trace,
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fetch agent is being set using the agent property in the options object, but cross-fetch does not support the agent property. This will have no effect and proxying will not work as intended.

Suggested change
? (url, options) => crossFetch(url, { ...options, agent } as RequestInit)
? (url, options) => crossFetch(url, { ...options, agent } as RequestInit)

AI-generated content by pr-review-commit incorrect_fetch_agent_option may be incorrect. Use reactions to eval.

: 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) => {
Expand Down
40 changes: 30 additions & 10 deletions packages/core/src/proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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`));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ProxyAgent from "undici" does not emit 'connect', 'connectionError', or 'disconnect' events. Attempting to attach event listeners to these events will have no effect and may cause confusion or runtime errors. Please refer to the "undici" documentation for supported events and usage.

AI-generated content by pr-review-commit undici_proxyagent_event_api may be incorrect. Use reactions to eval.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The event listeners connect, connectionError, and disconnect are being attached to the undici ProxyAgent, but these are not standard events for undici's ProxyAgent and will not be triggered. This may cause confusion or missed debugging information.

Suggested change
agent.on(`disconnect`, () => dbg(`disconnect`));
agent.on(`connect`, (info) => dbg(`connect: %s`, info.href));

AI-generated content by pr-review-commit undici_proxyagent_event_names may be incorrect. Use reactions to eval.

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()));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The event listeners connect and error are being attached to the https-proxy-agent, but these are not standard events for this agent and will not be triggered. This may cause confusion or missed debugging information.

Suggested change
agent.on(`error`, (err) => dbg(`error: %s`, err.toString()));
agent.on(`connect`, (info) => dbg(`connect: %s`, info.href));

AI-generated content by pr-review-commit https_proxy_agent_event_names may be incorrect. Use reactions to eval.

return agent;
}
20 changes: 20 additions & 0 deletions packages/sample/devproxyrc.json
Original file line number Diff line number Diff line change
@@ -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
}
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading