From 681a0f688fc3798dea009aa85933e871cae097d4 Mon Sep 17 00:00:00 2001 From: "Seongho.Bak" Date: Wed, 22 Jul 2026 11:44:49 +0900 Subject: [PATCH 1/3] fix(core): preserve AI proxy property references Fixes #22355 --- .../core/src/tracing/google-genai/index.ts | 20 ++++++++-- packages/core/src/tracing/openai/index.ts | 23 +++++++++--- .../test/tracing/ai-integration-proxy.test.ts | 37 +++++++++++++++++++ 3 files changed, 72 insertions(+), 8 deletions(-) create mode 100644 packages/core/test/tracing/ai-integration-proxy.test.ts diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index 68e4d414586a..9390a82e0e14 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -360,9 +360,16 @@ function instrumentMethod( * Recursively instruments methods and handles special cases like chats.create */ function createDeepProxy(target: T, currentPath = '', options: GoogleGenAIOptions): T { + const propertyCache = new Map(); + return new Proxy(target, { get: (t, prop, receiver) => { const value = Reflect.get(t, prop, receiver); + const cachedEntry = propertyCache.get(prop); + if (cachedEntry && cachedEntry.original === value) { + return cachedEntry.wrapped; + } + const methodPath = buildMethodPath(currentPath, String(prop)); const instrumentedMethod: InstrumentedMethodEntry | undefined = @@ -374,6 +381,7 @@ function createDeepProxy(target: T, currentPath = '', options: : value.bind(t); if (!instrumentedMethod.proxyResultPath) { + propertyCache.set(prop, { original: value, wrapped: wrappedMethod }); return wrappedMethod; } @@ -381,22 +389,28 @@ function createDeepProxy(target: T, currentPath = '', options: // Note: This currently only properly handles synchronous methods. For async methods, // the Promise itself would be proxied instead of the resolved value. Currently we // don't have a case where this is needed, so I'll keep it simple for now. - return function (...args: unknown[]): unknown { + const methodWithProxiedResult = function (...args: unknown[]): unknown { const result = wrappedMethod(...args); if (result && typeof result === 'object') { return createDeepProxy(result as object, instrumentedMethod.proxyResultPath, options); } return result; }; + propertyCache.set(prop, { original: value, wrapped: methodWithProxiedResult }); + return methodWithProxiedResult; } if (typeof value === 'function') { // Bind non-instrumented functions to preserve the original `this` context - return value.bind(t); + const boundMethod = value.bind(t); + propertyCache.set(prop, { original: value, wrapped: boundMethod }); + return boundMethod; } if (value && typeof value === 'object') { - return createDeepProxy(value, methodPath, options); + const nestedProxy = createDeepProxy(value, methodPath, options); + propertyCache.set(prop, { original: value, wrapped: nestedProxy }); + return nestedProxy; } return value; diff --git a/packages/core/src/tracing/openai/index.ts b/packages/core/src/tracing/openai/index.ts index 821e9c68e0ff..f5335253f14a 100644 --- a/packages/core/src/tracing/openai/index.ts +++ b/packages/core/src/tracing/openai/index.ts @@ -238,30 +238,43 @@ function instrumentMethod( * Create a deep proxy for OpenAI client instrumentation */ function createDeepProxy(target: T, currentPath = '', options: OpenAiOptions): T { + const propertyCache = new Map(); + return new Proxy(target, { - get(obj: object, prop: string): unknown { - const value = (obj as Record)[prop]; + get(obj: object, prop: string | symbol): unknown { + const value = Reflect.get(obj, prop) as unknown; + const cachedEntry = propertyCache.get(prop); + if (cachedEntry && cachedEntry.original === value) { + return cachedEntry.wrapped; + } + const methodPath = buildMethodPath(currentPath, String(prop)); const instrumentedMethod = OPENAI_METHOD_REGISTRY[methodPath as keyof typeof OPENAI_METHOD_REGISTRY]; if (typeof value === 'function' && instrumentedMethod) { - return instrumentMethod( + const wrappedMethod = instrumentMethod( value as (...args: unknown[]) => Promise, methodPath, instrumentedMethod, obj, options, ); + propertyCache.set(prop, { original: value, wrapped: wrappedMethod }); + return wrappedMethod; } if (typeof value === 'function') { // Bind non-instrumented functions to preserve the original `this` context, // which is required for accessing private class fields (e.g. #baseURL) in OpenAI SDK v5. - return value.bind(obj); + const boundMethod = value.bind(obj); + propertyCache.set(prop, { original: value, wrapped: boundMethod }); + return boundMethod; } if (value && typeof value === 'object') { - return createDeepProxy(value, methodPath, options); + const nestedProxy = createDeepProxy(value, methodPath, options); + propertyCache.set(prop, { original: value, wrapped: nestedProxy }); + return nestedProxy; } return value; diff --git a/packages/core/test/tracing/ai-integration-proxy.test.ts b/packages/core/test/tracing/ai-integration-proxy.test.ts new file mode 100644 index 000000000000..5d36884aa757 --- /dev/null +++ b/packages/core/test/tracing/ai-integration-proxy.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { instrumentGoogleGenAIClient } from '../../src/tracing/google-genai'; +import { instrumentOpenAiClient } from '../../src/tracing/openai'; + +describe('AI integration deep proxies', () => { + it('returns stable references for OpenAI client properties', () => { + const client = { + chat: { completions: { create: () => Promise.resolve({}) } }, + getBaseURL: () => 'https://api.openai.com', + }; + + const instrumentedClient = instrumentOpenAiClient(client); + const completions = instrumentedClient.chat.completions; + const create = completions.create; + const getBaseURL = instrumentedClient.getBaseURL; + + expect(instrumentedClient.chat.completions).toBe(completions); + expect(instrumentedClient.chat.completions.create).toBe(create); + expect(instrumentedClient.getBaseURL).toBe(getBaseURL); + }); + + it('returns stable references for Google GenAI client properties', () => { + const client = { + chats: { create: () => ({ sendMessage: () => Promise.resolve({}) }) }, + getVersion: () => '1.0.0', + }; + + const instrumentedClient = instrumentGoogleGenAIClient(client); + const chats = instrumentedClient.chats; + const create = chats.create; + const getVersion = instrumentedClient.getVersion; + + expect(instrumentedClient.chats).toBe(chats); + expect(instrumentedClient.chats.create).toBe(create); + expect(instrumentedClient.getVersion).toBe(getVersion); + }); +}); From 6f3a3a28d022f296b6b8e273493910f3b528d1d5 Mon Sep 17 00:00:00 2001 From: "Seongho.Bak" Date: Thu, 23 Jul 2026 14:33:14 +0900 Subject: [PATCH 2/3] chore: rerun CI From e9196e4878fa16cc152f7ed8983de56fa06ad2b4 Mon Sep 17 00:00:00 2001 From: "Seongho.Bak" Date: Thu, 23 Jul 2026 16:04:01 +0900 Subject: [PATCH 3/3] fix(core): invalidate stale AI proxy cache entries --- .../core/src/tracing/google-genai/index.ts | 3 ++ packages/core/src/tracing/openai/index.ts | 3 ++ .../test/tracing/ai-integration-proxy.test.ts | 35 +++++++++++++++++++ 3 files changed, 41 insertions(+) diff --git a/packages/core/src/tracing/google-genai/index.ts b/packages/core/src/tracing/google-genai/index.ts index 9390a82e0e14..1717433c1e28 100644 --- a/packages/core/src/tracing/google-genai/index.ts +++ b/packages/core/src/tracing/google-genai/index.ts @@ -369,6 +369,9 @@ function createDeepProxy(target: T, currentPath = '', options: if (cachedEntry && cachedEntry.original === value) { return cachedEntry.wrapped; } + if (cachedEntry) { + propertyCache.delete(prop); + } const methodPath = buildMethodPath(currentPath, String(prop)); diff --git a/packages/core/src/tracing/openai/index.ts b/packages/core/src/tracing/openai/index.ts index f5335253f14a..ec7fbe2e12c4 100644 --- a/packages/core/src/tracing/openai/index.ts +++ b/packages/core/src/tracing/openai/index.ts @@ -247,6 +247,9 @@ function createDeepProxy(target: T, currentPath = '', options: if (cachedEntry && cachedEntry.original === value) { return cachedEntry.wrapped; } + if (cachedEntry) { + propertyCache.delete(prop); + } const methodPath = buildMethodPath(currentPath, String(prop)); diff --git a/packages/core/test/tracing/ai-integration-proxy.test.ts b/packages/core/test/tracing/ai-integration-proxy.test.ts index 5d36884aa757..f27d090f2ba8 100644 --- a/packages/core/test/tracing/ai-integration-proxy.test.ts +++ b/packages/core/test/tracing/ai-integration-proxy.test.ts @@ -2,6 +2,19 @@ import { describe, expect, it } from 'vitest'; import { instrumentGoogleGenAIClient } from '../../src/tracing/google-genai'; import { instrumentOpenAiClient } from '../../src/tracing/openai'; +interface MutableClient { + service: { execute: () => Promise } | null; + getVersion: (() => string) | null; +} + +const clientInstrumenters: Array<{ + provider: string; + instrument: (client: MutableClient) => MutableClient; +}> = [ + { provider: 'OpenAI', instrument: client => instrumentOpenAiClient(client) }, + { provider: 'Google GenAI', instrument: client => instrumentGoogleGenAIClient(client) }, +]; + describe('AI integration deep proxies', () => { it('returns stable references for OpenAI client properties', () => { const client = { @@ -34,4 +47,26 @@ describe('AI integration deep proxies', () => { expect(instrumentedClient.chats.create).toBe(create); expect(instrumentedClient.getVersion).toBe(getVersion); }); + + it.each(clientInstrumenters)( + 'invalidates cached $provider references when source properties are cleared', + ({ instrument }) => { + const service = { execute: () => Promise.resolve() }; + const getVersion = () => '1.0.0'; + const client: MutableClient = { service, getVersion }; + const instrumentedClient = instrument(client); + const cachedService = instrumentedClient.service; + const cachedGetVersion = instrumentedClient.getVersion; + + client.service = null; + client.getVersion = null; + expect(instrumentedClient.service).toBeNull(); + expect(instrumentedClient.getVersion).toBeNull(); + + client.service = service; + client.getVersion = getVersion; + expect(instrumentedClient.service).not.toBe(cachedService); + expect(instrumentedClient.getVersion).not.toBe(cachedGetVersion); + }, + ); });