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
2 changes: 1 addition & 1 deletion packages/cli/cli-v2/build.dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ buildCli({
DEFAULT_FIDDLE_ORIGIN: "https://fiddle-coordinator.buildwithfern.com",
DEFAULT_VENUS_ORIGIN: "https://venus.buildwithfern.com",
DEFAULT_FDR_ORIGIN: "https://registry.buildwithfern.com",
DEFAULT_FDR_LAMBDA_DOCS_ORIGIN: "https://ykq45y6fvnszd35iv5yuuatkze0rpwuz.lambda-url.us-east-1.on.aws",
DEFAULT_FAI_ORIGIN: "https://fai.buildwithfern.com",
VENUS_AUDIENCE: "venus-prod",
LOCAL_STORAGE_FOLDER: ".fern",
POSTHOG_API_KEY: process.env.POSTHOG_API_KEY ?? "",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/cli/build.dev.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ buildCli({
DEFAULT_FIDDLE_ORIGIN: "https://fiddle-coordinator-dev2.buildwithfern.com",
DEFAULT_VENUS_ORIGIN: "https://venus-dev2.buildwithfern.com",
DEFAULT_FDR_ORIGIN: "https://registry-dev2.buildwithfern.com",
DEFAULT_FDR_LAMBDA_DOCS_ORIGIN: "https://ykq45y6fvnszd35iv5yuuatkze0rpwuz.lambda-url.us-east-1.on.aws",
DEFAULT_FAI_ORIGIN: "https://fai-dev2.buildwithfern.com",
VENUS_AUDIENCE: "venus-dev",
FERN_DASHBOARD_URL_DEFAULT: "https://dashboard-dev.buildwithfern.com",
LOCAL_STORAGE_FOLDER: ".fern-dev",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/cli/build.prod.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ buildCli({
DEFAULT_FIDDLE_ORIGIN: "https://fiddle-coordinator.buildwithfern.com",
DEFAULT_VENUS_ORIGIN: "https://venus.buildwithfern.com",
DEFAULT_FDR_ORIGIN: "https://registry.buildwithfern.com",
DEFAULT_FDR_LAMBDA_DOCS_ORIGIN: "https://ykq45y6fvnszd35iv5yuuatkze0rpwuz.lambda-url.us-east-1.on.aws",
DEFAULT_FAI_ORIGIN: "https://fai.buildwithfern.com",
VENUS_AUDIENCE: "venus-prod",
FERN_DASHBOARD_URL_DEFAULT: "https://dashboard.buildwithfern.com",
LOCAL_STORAGE_FOLDER: ".fern",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";

import { backfillMissingFields, unwrapLambdaBodyEnvelope } from "../enhanceExamplesWithAI.js";
import { backfillMissingFields, unwrapBodyEnvelope } from "../enhanceExamplesWithAI.js";

describe("backfillMissingFields", () => {
it("backfills missing fields from original into enhanced", () => {
Expand Down Expand Up @@ -79,30 +79,30 @@ describe("backfillMissingFields", () => {
});
});

describe("unwrapLambdaBodyEnvelope", () => {
describe("unwrapBodyEnvelope", () => {
it("unwraps {body: {...}} envelope", () => {
const wrapped = { body: { channelIds: [101, 202] } };
const result = unwrapLambdaBodyEnvelope(wrapped);
const result = unwrapBodyEnvelope(wrapped);
expect(result.wasWrapped).toBe(true);
expect(result.inner).toEqual({ channelIds: [101, 202] });
});

it("unwraps even when body is not the only key", () => {
const multiKey = { body: { a: 1 }, statusCode: 200 };
const result = unwrapLambdaBodyEnvelope(multiKey);
const result = unwrapBodyEnvelope(multiKey);
expect(result.wasWrapped).toBe(true);
expect(result.inner).toEqual({ a: 1 });
});

it("does not unwrap non-objects", () => {
expect(unwrapLambdaBodyEnvelope("hello")).toEqual({ wasWrapped: false, inner: "hello" });
expect(unwrapLambdaBodyEnvelope(null)).toEqual({ wasWrapped: false, inner: null });
expect(unwrapLambdaBodyEnvelope(42)).toEqual({ wasWrapped: false, inner: 42 });
expect(unwrapBodyEnvelope("hello")).toEqual({ wasWrapped: false, inner: "hello" });
expect(unwrapBodyEnvelope(null)).toEqual({ wasWrapped: false, inner: null });
expect(unwrapBodyEnvelope(42)).toEqual({ wasWrapped: false, inner: 42 });
});

it("does not unwrap arrays", () => {
const arr = [1, 2, 3];
expect(unwrapLambdaBodyEnvelope(arr)).toEqual({ wasWrapped: false, inner: arr });
expect(unwrapBodyEnvelope(arr)).toEqual({ wasWrapped: false, inner: arr });
});

it("backfills correctly through body envelope", () => {
Expand All @@ -114,7 +114,7 @@ describe("unwrapLambdaBodyEnvelope", () => {
channelIds: [1]
};

const unwrapped = unwrapLambdaBodyEnvelope(wrapped);
const unwrapped = unwrapBodyEnvelope(wrapped);
expect(unwrapped.wasWrapped).toBe(true);

const backfilled = backfillMissingFields(unwrapped.inner, original);
Expand All @@ -127,20 +127,20 @@ describe("unwrapLambdaBodyEnvelope", () => {
});

it("treats body as envelope only when original does NOT have body key", () => {
// Lambda envelope case: original has no "body" key → unwrap
// Envelope case: original has no "body" key → unwrap
const envelopeEnhanced = { body: { channelIds: [101] } };
const originalNoBody = { channelIds: [1], firstName: "string" };

const unwrapped = unwrapLambdaBodyEnvelope(envelopeEnhanced);
const unwrapped = unwrapBodyEnvelope(envelopeEnhanced);
expect(unwrapped.wasWrapped).toBe(true);
// Caller should check originalHasBody=false → treat as envelope

// Schema field case: original HAS a "body" key → not an envelope
const schemaEnhanced = { body: "Hello world", subject: "Greetings" };
const originalWithBody = { body: "string", subject: "string", to: "string" };

const unwrappedSchema = unwrapLambdaBodyEnvelope(schemaEnhanced);
// unwrapLambdaBodyEnvelope still returns wasWrapped=true (it's a detection helper),
const unwrappedSchema = unwrapBodyEnvelope(schemaEnhanced);
// unwrapBodyEnvelope still returns wasWrapped=true (it's a detection helper),
// but the caller checks originalHasBody and skips the envelope path.
expect(unwrappedSchema.wasWrapped).toBe(true);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { readFile, writeFile } from "fs/promises";
import * as yaml from "js-yaml";
import { OpenAPIV3 } from "openapi-types";
import { join } from "path";
import { FaiExampleEnhancer } from "./faiClient.js";
import { filterRequestBody, isFdrTypedValueWrapper, unwrapExampleValue } from "./filterHelpers.js";
import { LambdaExampleEnhancer } from "./lambdaClient.js";
import { SpinnerStatusCoordinator } from "./spinnerStatusCoordinator.js";
import {
AIExampleEnhancerConfig,
Expand Down Expand Up @@ -517,7 +517,7 @@ async function performAIEnhancement(
sourceSpecs?: OpenAPISourceSpec[],
apiName?: string
): Promise<FdrCjsSdk.api.v1.register.ApiDefinition> {
const enhancer = new LambdaExampleEnhancer(config, context, token, organizationId);
const enhancer = new FaiExampleEnhancer(config, context, token, organizationId);
const circuitBreaker = new CircuitBreaker();

let openApiSpec: string | undefined;
Expand Down Expand Up @@ -679,7 +679,7 @@ async function performAIEnhancement(

async function enhancePackageExamples(
apiDefinition: FdrCjsSdk.api.v1.register.ApiDefinition,
enhancer: LambdaExampleEnhancer,
enhancer: FaiExampleEnhancer,
context: TaskContext,
organizationId: string,
stats: { count: number; total: number },
Expand Down Expand Up @@ -893,7 +893,7 @@ function collectWorkItems(

async function processEndpointsConcurrently(
allWorkItems: (EndpointWorkItem & { packageId?: string })[],
enhancer: LambdaExampleEnhancer,
enhancer: FaiExampleEnhancer,
context: TaskContext,
organizationId: string,
stats: { count: number; total: number },
Expand All @@ -919,7 +919,7 @@ async function processEndpointsConcurrently(
const maxConcurrentRequests = parseInt(process.env.FERN_AI_MAX_CONCURRENT || "25", 10);

context.logger.debug(
`Processing ${allWorkItems.length} endpoints with max ${maxConcurrentRequests} concurrent Lambda calls using rolling window queue`
`Processing ${allWorkItems.length} endpoints with max ${maxConcurrentRequests} concurrent FAI calls using rolling window queue`
);

// Check circuit breaker before processing
Expand Down Expand Up @@ -1055,7 +1055,7 @@ async function processEndpointsConcurrently(

async function processEndpoint(
workItem: EndpointWorkItem & { packageId?: string },
enhancer: LambdaExampleEnhancer,
enhancer: FaiExampleEnhancer,
context: TaskContext,
organizationId: string,
stats: { count: number; total: number },
Expand Down Expand Up @@ -1111,22 +1111,22 @@ async function processEndpoint(
openApiSpec: prunedOpenApiSpec
};

// Single attempt - Lambda client handles retries internally (1 retry total)
// Single attempt - FAI client handles retries internally (1 retry total)
try {
const result = await enhancer.enhanceExample(request);

// Record success in circuit breaker
circuitBreaker?.recordSuccess();

// Backfill any required fields the AI may have missed from the original auto-generated example.
// The AI Lambda may not fully resolve nested allOf chains, producing partial examples.
// Handle the Lambda's {body: {...}} envelope if present, but only when the original
// The enhancement service may not fully resolve nested allOf chains, producing partial examples.
// Handle a {body: {...}} envelope if present, but only when the original
// does NOT have a "body" key (distinguishes envelope from schemas with a literal body field).
if (result.enhancedRequestExample != null && request.originalRequestExample != null) {
const unwrapped = unwrapLambdaBodyEnvelope(result.enhancedRequestExample);
const unwrapped = unwrapBodyEnvelope(result.enhancedRequestExample);
const originalHasBody = isObjectWithKey(request.originalRequestExample, "body");
if (unwrapped.wasWrapped && !originalHasBody) {
// Lambda envelope detected — backfill the inner value directly.
// Envelope detected — backfill the inner value directly.
// Do NOT re-wrap: the IR path only handles FDR {type,value} wrappers,
// and writeAiExamplesOverride has its own unwrap logic.
result.enhancedRequestExample = backfillMissingFields(unwrapped.inner, request.originalRequestExample);
Expand Down Expand Up @@ -1668,10 +1668,10 @@ function isObjectWithKey(value: unknown, key: string): boolean {
}

/**
* Detects and unwraps the Lambda's `{body: {...}}` envelope format.
* The Lambda sometimes wraps the request example in a "body" key.
* Detects and unwraps a `{body: {...}}` envelope format.
* The enhancement service sometimes wraps the request example in a "body" key.
*/
export function unwrapLambdaBodyEnvelope(value: unknown): { wasWrapped: boolean; inner: unknown } {
export function unwrapBodyEnvelope(value: unknown): { wasWrapped: boolean; inner: unknown } {
if (typeof value === "object" && value !== null && !Array.isArray(value) && "body" in value) {
return { wasWrapped: true, inner: (value as Record<string, unknown>).body };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ interface VenusJwtResponse {
expiresAt: string;
}

export class LambdaExampleEnhancer {
export class FaiExampleEnhancer {
private config: AIEnhancerResolvedConfig;
private context: TaskContext;
private lambdaOrigin: string;
private faiOrigin: string;
private venusOrigin: string;
private token: FernToken;
private jwtPromise: Promise<string> | undefined;
Expand All @@ -38,16 +38,16 @@ export class LambdaExampleEnhancer {
};
this.context = context;

// Get Lambda origin - throw error if not configured
const lambdaOrigin = process.env.DEFAULT_FDR_LAMBDA_DOCS_ORIGIN;
if (!lambdaOrigin) {
// Get FAI origin - throw error if not configured
const faiOrigin = process.env.DEFAULT_FAI_ORIGIN;
if (!faiOrigin) {
throw new CliError({
message:
"DEFAULT_FDR_LAMBDA_DOCS_ORIGIN environment variable is not set. AI example enhancement requires this to be configured.",
"DEFAULT_FAI_ORIGIN environment variable is not set. AI example enhancement requires this to be configured.",
code: CliError.Code.EnvironmentError
});
}
this.lambdaOrigin = lambdaOrigin;
this.faiOrigin = faiOrigin;

// Get Venus origin for JWT exchange
this.venusOrigin = process.env.DEFAULT_VENUS_ORIGIN ?? "https://venus.buildwithfern.com";
Expand Down Expand Up @@ -175,7 +175,7 @@ export class LambdaExampleEnhancer {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
this.context.logger.debug(
`Enhancing example for ${request.method} ${request.endpointPath} via lambda (attempt ${attempt}/${maxAttempts})`
`Enhancing example for ${request.method} ${request.endpointPath} via FAI (attempt ${attempt}/${maxAttempts})`
);

const requestBody = {
Expand Down Expand Up @@ -203,7 +203,7 @@ export class LambdaExampleEnhancer {
)}`
);

const response = await fetch(`${this.lambdaOrigin}/v2/registry/ai/enhance-example`, {
const response = await fetch(`${this.faiOrigin}/examples/enhance`, {
Comment thread
kafkas marked this conversation as resolved.
method: "POST",
headers: {
"Content-Type": "application/json",
Expand All @@ -216,7 +216,7 @@ export class LambdaExampleEnhancer {
if (!response.ok) {
const errorText = await response.text();
throw new CliError({
message: `Lambda returned ${response.status}: ${errorText || response.statusText}`,
message: `FAI returned ${response.status}: ${errorText || response.statusText}`,
code: CliError.Code.NetworkError
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ export async function writeAiExamplesOverride({
}

// Extract the inner value if the request body is wrapped in a "body" key
// The Lambda response sometimes wraps the request in { "body": { ... } }
// The enhancement service response sometimes wraps the request in { "body": { ... } }
let requestBodyToProcess = example.requestBody;
if (
requestBodyToProcess !== null &&
Expand Down
Loading