Skip to content

Commit 877fc42

Browse files
feat(instrumentation-google-generativeai): add OTel 1.40 GenAI semantic conventions instrumentation (#931)
1 parent 49a1544 commit 877fc42

File tree

21 files changed

+5252
-217
lines changed

21 files changed

+5252
-217
lines changed

packages/ai-semantic-conventions/src/SemanticAttributes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,5 @@ export const FinishReasons = {
162162
TOOL_CALL: "tool_call",
163163
CONTENT_FILTER: "content_filter",
164164
ERROR: "error",
165+
FINISH_REASON_UNSPECIFIED: "",
165166
} as const;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/dist
2+
/coverage
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# OpenTelemetry Google Gen AI SDK instrumentation for Node.js
2+
3+
[![NPM Published Version][npm-img]][npm-url]
4+
[![Apache License][license-image]][license-url]
5+
6+
This module provides automatic instrumentation for [`Google Gen AI SDK`](https://github.com/googleapis/js-genai) (`@google/genai`) module, which may be loaded using the [`@opentelemetry/sdk-trace-node`](https://github.com/open-telemetry/opentelemetry-js/tree/main/packages/opentelemetry-sdk-trace-node) package and is included in the [`@traceloop/node-server-sdk`](https://www.npmjs.com/package/@traceloop/node-server-sdk) bundle.
7+
8+
If total installation size is not constrained, it is recommended to use the [`@traceloop/node-server-sdk`](https://www.npmjs.com/package/@traceloop/node-server-sdk) bundle for the most seamless instrumentation experience.
9+
10+
Compatible with OpenTelemetry JS API and SDK `1.0+`.
11+
12+
## Installation
13+
14+
```bash
15+
npm install --save @traceloop/instrumentation-google-generativeai
16+
```
17+
18+
## Supported Versions
19+
20+
- `>=1.0.0`
21+
22+
## Usage
23+
24+
To load a specific plugin, specify it in the registerInstrumentations's configuration:
25+
26+
```js
27+
const { NodeTracerProvider } = require("@opentelemetry/sdk-trace-node");
28+
const {
29+
GenAIInstrumentation,
30+
} = require("@traceloop/instrumentation-google-generativeai");
31+
const { registerInstrumentations } = require("@opentelemetry/instrumentation");
32+
33+
const provider = new NodeTracerProvider();
34+
provider.register();
35+
36+
registerInstrumentations({
37+
instrumentations: [new GenAIInstrumentation()],
38+
});
39+
```
40+
41+
## Useful links
42+
43+
- For more information on OpenTelemetry, visit: <https://opentelemetry.io/>
44+
- For more about OpenTelemetry JavaScript: <https://github.com/open-telemetry/opentelemetry-js>
45+
- For help or feedback on this project, join us on [Slack][slack-url]
46+
47+
## License
48+
49+
Apache 2.0 - See [LICENSE][license-url] for more information.
50+
51+
[slack-url]: https://traceloop.com/slack
52+
[license-url]: https://github.com/traceloop/openllmetry-js/blob/main/LICENSE
53+
[license-image]: https://img.shields.io/badge/license-Apache_2.0-green.svg?style=flat
54+
[npm-url]: https://www.npmjs.com/package/@traceloop/instrumentation-google-generativeai
55+
[npm-img]: https://badge.fury.io/js/%40traceloop%2Finstrumentation-google-generativeai.svg
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
const js = require("@eslint/js");
2+
const rootConfig = require("../../eslint.config.cjs");
3+
4+
const { FlatCompat } = require("@eslint/eslintrc");
5+
6+
const compat = new FlatCompat({
7+
baseDirectory: __dirname,
8+
recommendedConfig: js.configs.recommended,
9+
allConfig: js.configs.all,
10+
});
11+
12+
module.exports = [
13+
{
14+
ignores: ["!**/*", "**/node_modules", "dist/**/*"],
15+
},
16+
...rootConfig,
17+
{
18+
files: ["**/*.ts", "**/*.tsx", "**/*.js", "**/*.jsx"],
19+
rules: {},
20+
},
21+
{
22+
files: ["**/*.ts", "**/*.tsx"],
23+
rules: {},
24+
},
25+
{
26+
files: ["**/*.js", "**/*.jsx"],
27+
rules: {},
28+
},
29+
];
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"name": "@traceloop/instrumentation-google-generativeai",
3+
"version": "0.24.1",
4+
"description": "Google Gen AI SDK Instrumentation",
5+
"main": "dist/index.js",
6+
"module": "dist/index.mjs",
7+
"types": "dist/index.d.ts",
8+
"repository": "traceloop/openllmetry-js",
9+
"scripts": {
10+
"build": "rollup -c",
11+
"lint": "eslint .",
12+
"lint:fix": "eslint . --fix",
13+
"test": "ts-mocha -p tsconfig.test.json 'tests/**/*.test.ts' --timeout 20000"
14+
},
15+
"keywords": [
16+
"opentelemetry",
17+
"nodejs",
18+
"tracing",
19+
"attributes",
20+
"semantic conventions",
21+
"google",
22+
"gemini",
23+
"genai"
24+
],
25+
"author": "Traceloop",
26+
"license": "Apache-2.0",
27+
"engines": {
28+
"node": ">=14"
29+
},
30+
"files": [
31+
"dist/**/*.js",
32+
"dist/**/*.mjs",
33+
"dist/**/*.js.map",
34+
"dist/**/*.d.ts",
35+
"doc",
36+
"LICENSE",
37+
"README.md",
38+
"package.json"
39+
],
40+
"publishConfig": {
41+
"access": "public"
42+
},
43+
"dependencies": {
44+
"@opentelemetry/api": "^1.9.0",
45+
"@opentelemetry/core": "^2.0.1",
46+
"@opentelemetry/instrumentation": "^0.203.0",
47+
"@opentelemetry/semantic-conventions": "^1.40.0",
48+
"@traceloop/ai-semantic-conventions": "workspace:*",
49+
"@traceloop/instrumentation-utils": "workspace:*",
50+
"tslib": "^2.8.1"
51+
},
52+
"devDependencies": {
53+
"@google/genai": "^1.50.1",
54+
"@opentelemetry/context-async-hooks": "^2.0.1",
55+
"@opentelemetry/sdk-trace-base": "^2.0.1",
56+
"@opentelemetry/sdk-trace-node": "^2.0.1",
57+
"@types/mocha": "^10.0.10",
58+
"ts-mocha": "^11.1.0"
59+
},
60+
"homepage": "https://github.com/traceloop/openllmetry-js/tree/main/packages/instrumentation-google-generativeai"
61+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const dts = require("rollup-plugin-dts");
2+
const typescript = require("@rollup/plugin-typescript");
3+
const json = require("@rollup/plugin-json");
4+
5+
const name = require("./package.json").main.replace(/\.js$/, "");
6+
7+
const bundle = (config) => ({
8+
...config,
9+
input: "src/index.ts",
10+
external: (id) => !/^[./]/.test(id),
11+
});
12+
13+
exports.default = [
14+
bundle({
15+
plugins: [
16+
typescript.default({ exclude: ["test/**/*", "tests/**/*"] }),
17+
json.default(),
18+
],
19+
output: [
20+
{
21+
file: `${name}.js`,
22+
format: "cjs",
23+
sourcemap: true,
24+
},
25+
{
26+
file: `${name}.mjs`,
27+
format: "es",
28+
sourcemap: true,
29+
},
30+
],
31+
}),
32+
bundle({
33+
plugins: [dts.default({ exclude: ["test/**/*", "tests/**/*"] })],
34+
output: {
35+
file: `${name}.d.ts`,
36+
format: "es",
37+
},
38+
}),
39+
];
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
/*
2+
* Copyright Traceloop
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import type * as genai from "@google/genai";
18+
19+
// Maps a single @google/genai Part to its OTel-compliant part object.
20+
//
21+
// text (thought: true) → ReasoningPart { type: "reasoning", content }
22+
// text → TextPart
23+
// inlineData (base64) → BlobPart { modality derived from mimeType, mime_type, content }
24+
// fileData (URI) → UriPart { modality derived from mimeType, mime_type, uri }
25+
// functionCall → ToolCallRequestPart
26+
// functionResponse → ToolCallResponsePart
27+
// executableCode → GenericPart { type: "executable_code", language, content }
28+
// codeExecutionResult → GenericPart { type: "code_execution_result", outcome, content }
29+
// <unknown> → GenericPart
30+
31+
/** OTel gen_ai part type strings used by the GenAI mapper. */
32+
export const GenAIOtelPartType = {
33+
TEXT: "text",
34+
REASONING: "reasoning",
35+
BLOB: "blob",
36+
URI: "uri",
37+
TOOL_CALL: "tool_call",
38+
TOOL_CALL_RESPONSE: "tool_call_response",
39+
EXECUTABLE_CODE: "executable_code",
40+
CODE_EXECUTION_RESULT: "code_execution_result",
41+
UNKNOWN: "unknown",
42+
} as const;
43+
44+
function genaiModalityFromMimeType(mimeType: string): string {
45+
if (mimeType.startsWith("image/")) return "image";
46+
if (mimeType.startsWith("audio/")) return "audio";
47+
if (mimeType.startsWith("video/")) return "video";
48+
return "document";
49+
}
50+
51+
export function mapGenAIContentBlock(block: genai.Part | string): object {
52+
if (typeof block === "string") {
53+
return { type: GenAIOtelPartType.TEXT, content: block };
54+
}
55+
56+
// thought: true marks a model reasoning/thinking block (Gemini thinking models).
57+
// text may be undefined on malformed parts — fall back to empty string.
58+
if (block.thought === true) {
59+
return { type: GenAIOtelPartType.REASONING, content: block.text ?? "" };
60+
}
61+
62+
if (block.text !== undefined) {
63+
return { type: GenAIOtelPartType.TEXT, content: block.text };
64+
}
65+
66+
if (block.inlineData) {
67+
const mimeType = block.inlineData.mimeType;
68+
return {
69+
type: GenAIOtelPartType.BLOB,
70+
modality: genaiModalityFromMimeType(mimeType ?? ""),
71+
...(mimeType ? { mime_type: mimeType } : {}),
72+
content: block.inlineData.data,
73+
};
74+
}
75+
76+
if (block.fileData) {
77+
const mimeType = block.fileData.mimeType;
78+
return {
79+
type: GenAIOtelPartType.URI,
80+
modality: genaiModalityFromMimeType(mimeType ?? ""),
81+
...(mimeType ? { mime_type: mimeType } : {}),
82+
uri: block.fileData.fileUri,
83+
};
84+
}
85+
86+
if (block.functionCall) {
87+
return {
88+
type: GenAIOtelPartType.TOOL_CALL,
89+
id: block.functionCall.id ?? null,
90+
name: block.functionCall.name,
91+
arguments: block.functionCall.args,
92+
};
93+
}
94+
95+
if (block.functionResponse) {
96+
const resp = block.functionResponse.response;
97+
return {
98+
type: GenAIOtelPartType.TOOL_CALL_RESPONSE,
99+
id: block.functionResponse.id ?? null,
100+
// Serialize objects to JSON string so the dashboard can display them.
101+
// Normalize undefined (missing response) to null so the field is always present.
102+
response:
103+
resp === undefined
104+
? null
105+
: resp !== null && typeof resp === "object"
106+
? JSON.stringify(resp)
107+
: resp,
108+
};
109+
}
110+
111+
if (block.executableCode) {
112+
return {
113+
type: GenAIOtelPartType.EXECUTABLE_CODE,
114+
language: block.executableCode.language,
115+
content: block.executableCode.code,
116+
};
117+
}
118+
119+
if (block.codeExecutionResult) {
120+
return {
121+
type: GenAIOtelPartType.CODE_EXECUTION_RESULT,
122+
outcome: block.codeExecutionResult.outcome,
123+
content: block.codeExecutionResult.output,
124+
};
125+
}
126+
127+
// Do not spread the block — it may contain circular references or sensitive data.
128+
return { type: GenAIOtelPartType.UNKNOWN };
129+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { GenAIInstrumentation, genaiFinishReasonMap } from "./instrumentation";
2+
export type { GenAIInstrumentationConfig } from "./types";

0 commit comments

Comments
 (0)