-
Notifications
You must be signed in to change notification settings - Fork 44
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
feat(js): OpenAI embeddings Instrumentation #34
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@arizeai/openinference-instrumentation-openai": patch | ||
"@arizeai/openinference-semantic-conventions": patch | ||
--- | ||
|
||
Add OpenAI Embeddings sementic attributes and instrumentation |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,4 +2,5 @@ | |
module.exports = { | ||
preset: "ts-jest", | ||
testEnvironment: "node", | ||
prettierPath: null, | ||
}; |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -26,6 +26,10 @@ import { | |||||||||
ChatCompletionCreateParamsBase, | ||||||||||
} from "openai/resources/chat/completions"; | ||||||||||
import { Stream } from "openai/streaming"; | ||||||||||
import { | ||||||||||
CreateEmbeddingResponse, | ||||||||||
EmbeddingCreateParams, | ||||||||||
} from "openai/resources"; | ||||||||||
|
||||||||||
const MODULE_NAME = "openai"; | ||||||||||
|
||||||||||
|
@@ -80,7 +84,7 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> { | |||||||||
const span = instrumentation.tracer.startSpan( | ||||||||||
`OpenAI Chat Completions`, | ||||||||||
{ | ||||||||||
kind: SpanKind.CLIENT, | ||||||||||
kind: SpanKind.INTERNAL, | ||||||||||
attributes: { | ||||||||||
[SemanticConventions.OPENINFERENCE_SPAN_KIND]: | ||||||||||
OpenInferenceSpanKind.LLM, | ||||||||||
|
@@ -106,6 +110,11 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> { | |||||||||
// Push the error to the span | ||||||||||
if (error) { | ||||||||||
span.recordException(error); | ||||||||||
span.setStatus({ | ||||||||||
code: SpanStatusCode.ERROR, | ||||||||||
message: error.message, | ||||||||||
}); | ||||||||||
span.end(); | ||||||||||
} | ||||||||||
}, | ||||||||||
); | ||||||||||
|
@@ -115,6 +124,12 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> { | |||||||||
span.setAttributes({ | ||||||||||
[SemanticConventions.OUTPUT_VALUE]: JSON.stringify(result), | ||||||||||
[SemanticConventions.OUTPUT_MIME_TYPE]: MimeType.JSON, | ||||||||||
// Override the model from the value sent by the server | ||||||||||
[SemanticConventions.LLM_MODEL_NAME]: isChatCompletionResponse( | ||||||||||
result, | ||||||||||
) | ||||||||||
? result.model | ||||||||||
: body.model, | ||||||||||
...getLLMOutputMessagesAttributes(result), | ||||||||||
...getUsageAttributes(result), | ||||||||||
}); | ||||||||||
|
@@ -127,6 +142,75 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> { | |||||||||
}; | ||||||||||
}, | ||||||||||
); | ||||||||||
|
||||||||||
// Patch embeddings | ||||||||||
type EmbeddingsCreateType = | ||||||||||
typeof module.OpenAI.Embeddings.prototype.create; | ||||||||||
this._wrap( | ||||||||||
module.OpenAI.Embeddings.prototype, | ||||||||||
"create", | ||||||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||||||||||
(original: EmbeddingsCreateType): any => { | ||||||||||
return function patchedEmbeddingCreate( | ||||||||||
this: unknown, | ||||||||||
...args: Parameters<typeof module.OpenAI.Embeddings.prototype.create> | ||||||||||
) { | ||||||||||
const body = args[0]; | ||||||||||
const { input } = body; | ||||||||||
const isStringInput = typeof input == "string"; | ||||||||||
const span = instrumentation.tracer.startSpan(`OpenAI Embeddings`, { | ||||||||||
kind: SpanKind.INTERNAL, | ||||||||||
attributes: { | ||||||||||
[SemanticConventions.OPENINFERENCE_SPAN_KIND]: | ||||||||||
OpenInferenceSpanKind.EMBEDDING, | ||||||||||
[SemanticConventions.EMBEDDING_MODEL_NAME]: body.model, | ||||||||||
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. Probably want to capture 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. I need to double-check as a follow-up on Azure. It's a bit different in JS. They have a supplamentary package. I'll file a follow-up ticket for that. |
||||||||||
[SemanticConventions.INPUT_VALUE]: isStringInput | ||||||||||
? input | ||||||||||
: JSON.stringify(input), | ||||||||||
[SemanticConventions.INPUT_MIME_TYPE]: isStringInput | ||||||||||
? MimeType.TEXT | ||||||||||
: MimeType.JSON, | ||||||||||
...getEmbeddingTextAttributes(body), | ||||||||||
}, | ||||||||||
}); | ||||||||||
const execContext = trace.setSpan(context.active(), span); | ||||||||||
const execPromise = safeExecuteInTheMiddle< | ||||||||||
ReturnType<EmbeddingsCreateType> | ||||||||||
>( | ||||||||||
() => { | ||||||||||
return context.with(execContext, () => { | ||||||||||
return original.apply(this, args); | ||||||||||
}); | ||||||||||
}, | ||||||||||
(error) => { | ||||||||||
// Push the error to the span | ||||||||||
if (error) { | ||||||||||
span.recordException(error); | ||||||||||
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. Recording exceptions doesn't actually update the status, because a span can record multiple exceptions such as retry errors and still turn out
Suggested change
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. True, I was thinking of leaving it mimimal - I'll at least set the status code. Might leave off the span end for now. Not sure if that's necessary. Will look into it a bit more. 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. Oh actually I can put it below. I'll do that. |
||||||||||
span.setStatus({ | ||||||||||
code: SpanStatusCode.ERROR, | ||||||||||
message: error.message, | ||||||||||
}); | ||||||||||
span.end(); | ||||||||||
} | ||||||||||
}, | ||||||||||
); | ||||||||||
const wrappedPromise = execPromise.then((result) => { | ||||||||||
if (result) { | ||||||||||
// Record the results | ||||||||||
span.setAttributes({ | ||||||||||
// Do not record the output data as it can be large | ||||||||||
...getEmbeddingEmbeddingsAttributes(result), | ||||||||||
}); | ||||||||||
} | ||||||||||
span.setStatus({ code: SpanStatusCode.OK }); | ||||||||||
span.end(); | ||||||||||
return result; | ||||||||||
}); | ||||||||||
return context.bind(execContext, wrappedPromise); | ||||||||||
}; | ||||||||||
}, | ||||||||||
); | ||||||||||
|
||||||||||
module.openInferencePatched = true; | ||||||||||
return module; | ||||||||||
} | ||||||||||
|
@@ -136,9 +220,19 @@ export class OpenAIInstrumentation extends InstrumentationBase<typeof openai> { | |||||||||
private unpatch(moduleExports: typeof openai, moduleVersion?: string) { | ||||||||||
diag.debug(`Removing patch for ${MODULE_NAME}@${moduleVersion}`); | ||||||||||
this._unwrap(moduleExports.OpenAI.Chat.Completions.prototype, "create"); | ||||||||||
this._unwrap(moduleExports.OpenAI.Embeddings.prototype, "create"); | ||||||||||
} | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* type-guard that checks if the response is a chat completion response | ||||||||||
*/ | ||||||||||
function isChatCompletionResponse( | ||||||||||
response: Stream<ChatCompletionChunk> | ChatCompletion, | ||||||||||
): response is ChatCompletion { | ||||||||||
return "choices" in response; | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Converts the body of the request to LLM input messages | ||||||||||
*/ | ||||||||||
|
@@ -204,3 +298,43 @@ function getLLMOutputMessagesAttributes( | |||||||||
} | ||||||||||
return {}; | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Converts the embedding result payload to embedding attributes | ||||||||||
*/ | ||||||||||
function getEmbeddingTextAttributes( | ||||||||||
request: EmbeddingCreateParams, | ||||||||||
): Attributes { | ||||||||||
if (typeof request.input == "string") { | ||||||||||
return { | ||||||||||
[`${SemanticConventions.EMBEDDING_EMBEDDINGS}.0.${SemanticConventions.EMBEDDING_TEXT}`]: | ||||||||||
request.input, | ||||||||||
}; | ||||||||||
} else if ( | ||||||||||
Array.isArray(request.input) && | ||||||||||
request.input.length > 0 && | ||||||||||
typeof request.input[0] == "string" | ||||||||||
) { | ||||||||||
return request.input.reduce((acc, input, index) => { | ||||||||||
const index_prefix = `${SemanticConventions.EMBEDDING_EMBEDDINGS}.${index}`; | ||||||||||
acc[`${index_prefix}.${SemanticConventions.EMBEDDING_TEXT}`] = input; | ||||||||||
return acc; | ||||||||||
}, {} as Attributes); | ||||||||||
} | ||||||||||
// Ignore other cases where input is a number or an array of numbers | ||||||||||
return {}; | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Converts the embedding result payload to embedding attributes | ||||||||||
*/ | ||||||||||
function getEmbeddingEmbeddingsAttributes( | ||||||||||
response: CreateEmbeddingResponse, | ||||||||||
): Attributes { | ||||||||||
return response.data.reduce((acc, embedding, index) => { | ||||||||||
const index_prefix = `${SemanticConventions.EMBEDDING_EMBEDDINGS}.${index}`; | ||||||||||
acc[`${index_prefix}.${SemanticConventions.EMBEDDING_VECTOR}`] = | ||||||||||
embedding.embedding; | ||||||||||
return acc; | ||||||||||
}, {} as Attributes); | ||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
jest doesn't support prettier 3