-
Notifications
You must be signed in to change notification settings - Fork 138
feat: openai-js url path #2085
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
GeLi2001
wants to merge
14
commits into
main
Choose a base branch
from
feat/openai-url-path
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+231
−0
Open
feat: openai-js url path #2085
Changes from all commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
2f2e266
feat: openai-js url path
GeLi2001 882f78a
wip
GeLi2001 b19770a
wip
GeLi2001 4f6320e
wi[
GeLi2001 fadb104
wip
GeLi2001 1ae0a84
cleanup
GeLi2001 cb195e4
wip
GeLi2001 f0b2cf5
wip
GeLi2001 dc48f40
prettier
GeLi2001 9620b9f
wip
GeLi2001 f1c7374
wip
GeLi2001 145a900
wip
GeLi2001 b8eb005
Add URL extraction for Azure OpenAI debugging
GeLi2001 d4f3297
wip
GeLi2001 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -70,6 +70,12 @@ const INSTRUMENTATION_NAME = "@arizeai/openinference-instrumentation-openai"; | |
*/ | ||
let _isOpenInferencePatched = false; | ||
|
||
/** | ||
* Map to store URL information for each request using trace context | ||
* Uses trace ID + span ID as the key to avoid concurrent request overwrites | ||
*/ | ||
const requestUrlMap = new Map<string, { url: string; baseUrl?: string }>(); | ||
|
||
/** | ||
* function to check if instrumentation is enabled / disabled | ||
*/ | ||
|
@@ -95,6 +101,128 @@ function getExecContext(span: Span) { | |
return execContext; | ||
} | ||
|
||
/** | ||
* Extracts URL attributes for debugging purposes (especially useful for Azure) | ||
* @param fullUrl The complete URL of the request | ||
* @param baseUrl The base URL of the client | ||
* @returns Object containing URL attributes for debugging | ||
*/ | ||
function getUrlAttributes( | ||
fullUrl: string, | ||
baseUrl?: string, | ||
): Record<string, string> { | ||
const attributes: Record<string, string> = {}; | ||
|
||
try { | ||
const url = new URL(fullUrl); | ||
|
||
// Always include the full URL for complete debugging context | ||
attributes["url.full"] = fullUrl; | ||
|
||
// Extract the path component | ||
if (baseUrl) { | ||
try { | ||
const baseUrlObj = new URL(baseUrl); | ||
const fullUrlObj = new URL(fullUrl); | ||
|
||
// If the hosts match, calculate the path difference | ||
if (baseUrlObj.hostname === fullUrlObj.hostname) { | ||
// For Azure OpenAI, we want to reconstruct the deployment path | ||
// baseUrl example: "https://example.openai.azure.com/openai/deployments/gpt-4" | ||
// fullUrl example: "https://example.openai.azure.com/chat/completions" | ||
// We want to extract the deployment info from baseUrl and combine with the endpoint | ||
|
||
const basePath = baseUrlObj.pathname; | ||
const fullPath = fullUrlObj.pathname; | ||
|
||
// Extract deployment information from the base URL | ||
if (basePath.includes("/deployments/")) { | ||
// Extract the deployment part: "deployments/model-name" | ||
const deploymentMatch = basePath.match(/\/deployments\/([^/]+)/); | ||
if (deploymentMatch) { | ||
const deploymentName = deploymentMatch[1]; | ||
const endpoint = fullPath.startsWith("/") | ||
? fullPath.substring(1) | ||
: fullPath; | ||
attributes["url.path"] = | ||
`deployments/${deploymentName}/${endpoint}`; | ||
} else { | ||
// Fallback to just the endpoint | ||
attributes["url.path"] = fullPath.startsWith("/") | ||
? fullPath.substring(1) | ||
: fullPath; | ||
} | ||
} else { | ||
// Not a deployment URL, use the full path | ||
attributes["url.path"] = fullPath.startsWith("/") | ||
? fullPath.substring(1) | ||
: fullPath; | ||
} | ||
} else { | ||
// Different hosts, use pathname without leading slash | ||
const pathname = url.pathname.startsWith("/") | ||
? url.pathname.substring(1) | ||
: url.pathname; | ||
attributes["url.path"] = pathname || "/"; | ||
} | ||
} catch { | ||
// If URL parsing fails, use the pathname | ||
const pathname = url.pathname.startsWith("/") | ||
? url.pathname.substring(1) | ||
: url.pathname; | ||
attributes["url.path"] = pathname || "/"; | ||
} | ||
} else { | ||
const pathname = url.pathname.startsWith("/") | ||
? url.pathname.substring(1) | ||
: url.pathname; | ||
attributes["url.path"] = pathname || "/"; | ||
} | ||
|
||
// Safely extract api_version query parameter for Azure | ||
if (url.search) { | ||
const queryParams = new URLSearchParams(url.search); | ||
const apiVersion = queryParams.get("api-version"); | ||
if (apiVersion) { | ||
attributes["url.query.api_version"] = apiVersion; | ||
} | ||
} | ||
} catch (error) { | ||
diag.debug("Failed to extract URL attributes", error); | ||
} | ||
|
||
return attributes; | ||
} | ||
|
||
/** | ||
* Gets URL attributes for a request from stored request information | ||
* @param span The span to get URL attributes for | ||
* @returns URL attributes object | ||
*/ | ||
function getStoredUrlAttributes(span: Span): Record<string, string> { | ||
try { | ||
const spanContext = span.spanContext(); | ||
const contextKey = `${spanContext.traceId}-${spanContext.spanId}`; | ||
const urlInfo = requestUrlMap.get(contextKey); | ||
if (urlInfo) { | ||
diag.debug("Retrieved URL info from requestUrlMap", { | ||
urlInfo, | ||
contextKey, | ||
}); | ||
// Clean up after use to prevent memory leaks | ||
requestUrlMap.delete(contextKey); | ||
return getUrlAttributes(urlInfo.url, urlInfo.baseUrl); | ||
} else { | ||
diag.debug("No URL info found in requestUrlMap for this span", { | ||
contextKey, | ||
}); | ||
} | ||
} catch (error) { | ||
diag.debug("Failed to get stored URL attributes", error); | ||
} | ||
return {}; | ||
} | ||
|
||
/** | ||
* Gets the appropriate LLM provider based on the OpenAI client instance | ||
* Follows the same logic as the Python implementation by checking the baseURL host | ||
|
@@ -256,6 +384,87 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> { | |
// eslint-disable-next-line @typescript-eslint/no-this-alias | ||
const instrumentation: OpenAIInstrumentation = this; | ||
|
||
// Patch the post method to capture URL information | ||
this._wrap( | ||
module.OpenAI.prototype, | ||
"post", | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
(original: any): any => { | ||
return function patchedPost( | ||
this: unknown, | ||
path: string, | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
body?: any, | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
options?: any, | ||
) { | ||
// Store URL information for this specific request | ||
try { | ||
const clientInstance = this as { | ||
baseURL?: string; | ||
_client?: { baseURL?: string }; | ||
}; | ||
|
||
let baseUrl: string | undefined; | ||
if ( | ||
clientInstance.baseURL && | ||
typeof clientInstance.baseURL === "string" | ||
) { | ||
baseUrl = clientInstance.baseURL; | ||
} else if ( | ||
clientInstance._client?.baseURL && | ||
typeof clientInstance._client.baseURL === "string" | ||
) { | ||
baseUrl = clientInstance._client.baseURL; | ||
} | ||
|
||
if (baseUrl) { | ||
// Construct the full URL with query parameters if available | ||
let fullUrl = new URL(path, baseUrl).toString(); | ||
|
||
// Add query parameters if they exist in options | ||
if (options?.query && typeof options.query === "object") { | ||
const url = new URL(fullUrl); | ||
Object.entries(options.query).forEach(([key, value]) => { | ||
if (value !== undefined && value !== null) { | ||
url.searchParams.set(key, String(value)); | ||
} | ||
}); | ||
fullUrl = url.toString(); | ||
} | ||
|
||
// Store URL info using the current active span context | ||
const activeSpan = trace.getActiveSpan(); | ||
if (activeSpan) { | ||
const spanContext = activeSpan.spanContext(); | ||
const contextKey = `${spanContext.traceId}-${spanContext.spanId}`; | ||
requestUrlMap.set(contextKey, { url: fullUrl, baseUrl }); | ||
diag.debug("Stored URL info for request", { | ||
fullUrl, | ||
baseUrl, | ||
contextKey, | ||
}); | ||
// Clean up old entries to prevent memory leaks | ||
if (requestUrlMap.size > 1000) { | ||
const oldestKey = requestUrlMap.keys().next().value; | ||
if (oldestKey) { | ||
requestUrlMap.delete(oldestKey); | ||
} | ||
} | ||
} | ||
} | ||
} catch (error) { | ||
diag.debug( | ||
"Failed to capture URL information in post method", | ||
error, | ||
); | ||
} | ||
|
||
return original.apply(this, [path, body, options]); | ||
}; | ||
cursor[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||
}, | ||
); | ||
GeLi2001 marked this conversation as resolved.
Show resolved
Hide resolved
GeLi2001 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
// Patch create chat completions | ||
type ChatCompletionCreateType = | ||
typeof module.OpenAI.Chat.Completions.prototype.create; | ||
|
@@ -324,6 +533,8 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> { | |
[SemanticConventions.LLM_MODEL_NAME]: result.model, | ||
...getChatCompletionLLMOutputMessagesAttributes(result), | ||
...getUsageAttributes(result), | ||
// Add URL attributes now that the request has completed | ||
...getStoredUrlAttributes(span), | ||
}); | ||
span.setStatus({ code: SpanStatusCode.OK }); | ||
span.end(); | ||
|
@@ -410,6 +621,8 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> { | |
[SemanticConventions.LLM_MODEL_NAME]: result.model, | ||
...getCompletionOutputValueAndMimeType(result), | ||
...getUsageAttributes(result), | ||
// Add URL attributes now that the request has completed | ||
...getStoredUrlAttributes(span), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
}); | ||
span.setStatus({ code: SpanStatusCode.OK }); | ||
span.end(); | ||
|
@@ -481,6 +694,8 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> { | |
span.setAttributes({ | ||
// Do not record the output data as it can be large | ||
...getEmbeddingEmbeddingsAttributes(result), | ||
// Add URL attributes now that the request has completed | ||
...getStoredUrlAttributes(span), | ||
}); | ||
} | ||
span.setStatus({ code: SpanStatusCode.OK }); | ||
|
@@ -566,6 +781,8 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> { | |
[SemanticConventions.LLM_MODEL_NAME]: result.model, | ||
...getResponsesOutputMessagesAttributes(result), | ||
...getResponsesUsageAttributes(result), | ||
// Add URL attributes now that the request has completed | ||
...getStoredUrlAttributes(span), | ||
}); | ||
span.setStatus({ code: SpanStatusCode.OK }); | ||
span.end(); | ||
|
@@ -614,6 +831,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> { | |
moduleVersion?: string, | ||
) { | ||
diag.debug(`Removing patch for ${MODULE_NAME}@${moduleVersion}`); | ||
this._unwrap(moduleExports.OpenAI.prototype, "post"); | ||
this._unwrap(moduleExports.OpenAI.Chat.Completions.prototype, "create"); | ||
this._unwrap(moduleExports.OpenAI.Completions.prototype, "create"); | ||
this._unwrap(moduleExports.OpenAI.Embeddings.prototype, "create"); | ||
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.