pi-langfuse 1.5.1 → 1.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -0
- package/package.json +1 -1
- package/src/handlers/agent.ts +5 -0
- package/src/handlers/generation.ts +8 -0
- package/src/langfuse.ts +7 -1
- package/src/source-metadata.ts +160 -0
- package/src/types.ts +3 -0
- package/src/utils.ts +94 -2
package/README.md
CHANGED
|
@@ -150,6 +150,53 @@ The package also includes a Langfuse CLI skill, so Langfuse data can be queried
|
|
|
150
150
|
/pi-langfuse-langfuse <your-query>
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
+
## Source Metadata
|
|
154
|
+
|
|
155
|
+
Local prototype note: source metadata support in this installed package is a local prototype patch. A durable solution should be shipped through an upstream PR, a fork, or a maintained package version so reinstalling the extension does not lose the behavior.
|
|
156
|
+
|
|
157
|
+
For Git-backed runs, the extension attaches safe source metadata to traces:
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
{
|
|
161
|
+
"source_type": "git-repo",
|
|
162
|
+
"repo_identity": "owner/repo",
|
|
163
|
+
"repo_owner": "owner",
|
|
164
|
+
"repo_name": "repo",
|
|
165
|
+
"repo_root_name": "repo",
|
|
166
|
+
"git_branch": "main",
|
|
167
|
+
"git_commit": "abc123",
|
|
168
|
+
"git_remote_host": "github.com",
|
|
169
|
+
"git_remote_path": "owner/repo",
|
|
170
|
+
"metadata_source": "git-detection"
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
`repo_identity` is `owner/repo`. `repo_name` is the repo name only and must not contain a slash.
|
|
175
|
+
|
|
176
|
+
A Git repo may optionally provide `.pi-langfuse.metadata.json`. Overrides are whitelist-only; unknown keys are ignored. Allowed keys are:
|
|
177
|
+
|
|
178
|
+
```text
|
|
179
|
+
repo_identity
|
|
180
|
+
repo_owner
|
|
181
|
+
repo_name
|
|
182
|
+
source_type
|
|
183
|
+
service_name
|
|
184
|
+
project_slug
|
|
185
|
+
environment
|
|
186
|
+
observability_owner
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Repo-local overrides are used only after the working directory is confirmed to be inside a usable Git repo. If Git detection fails for any reason, including a missing Git command, corrupted repo, or non-Git folder, the extension ignores repo-local identity files and emits only:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"source_type": "non-git",
|
|
194
|
+
"metadata_source": "non-git"
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
The extension must not upload raw absolute local paths, credentialed remotes, tokens, unknown override keys, or folder names for non-Git folders.
|
|
199
|
+
|
|
153
200
|
## Troubleshooting
|
|
154
201
|
|
|
155
202
|
### No traces appearing?
|
package/package.json
CHANGED
package/src/handlers/agent.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { ensureConfig } from "../config.js";
|
|
|
4
4
|
import { shapePayload, truncate, extractFinalAssistant, extractAssistantOutput, getCapturePolicy } from "../utils.js";
|
|
5
5
|
import { closeDanglingObservations } from "./tool.js";
|
|
6
6
|
import { applyCapturePolicy } from "../capture-policy.js";
|
|
7
|
+
import { collectSourceMetadata } from "../source-metadata.js";
|
|
7
8
|
|
|
8
9
|
function stringMetadata(metadata: Record<string, unknown> | undefined): Record<string, string> | undefined {
|
|
9
10
|
if (!metadata) {
|
|
@@ -67,11 +68,13 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
|
67
68
|
images: event.images,
|
|
68
69
|
context: event.context ?? event.attachments,
|
|
69
70
|
});
|
|
71
|
+
const sourceMetadata = collectSourceMetadata(cwd);
|
|
70
72
|
const captured = applyCapturePolicy(
|
|
71
73
|
{
|
|
72
74
|
input: rawPromptInput,
|
|
73
75
|
metadata: {
|
|
74
76
|
cwd,
|
|
77
|
+
...sourceMetadata,
|
|
75
78
|
...(state.currentModel ? { model: state.currentModel } : {}),
|
|
76
79
|
...(state.currentProvider ? { provider: state.currentProvider } : {}),
|
|
77
80
|
sessionId: state.currentSessionId || undefined,
|
|
@@ -88,6 +91,7 @@ export async function startAgentRun(event: Record<string, unknown>, ctx: any) {
|
|
|
88
91
|
activeGenerations: new Map(),
|
|
89
92
|
generationOrder: [],
|
|
90
93
|
activeTools: new Map(),
|
|
94
|
+
sourceMetadata,
|
|
91
95
|
providerMetadataByRequest: new Map(),
|
|
92
96
|
};
|
|
93
97
|
|
|
@@ -133,6 +137,7 @@ export async function finishAgentRun(event: Record<string, unknown> = {}) {
|
|
|
133
137
|
output: rawOutput,
|
|
134
138
|
metadata: {
|
|
135
139
|
cwd: state.agentState.cwd,
|
|
140
|
+
...(state.agentState.sourceMetadata ?? {}),
|
|
136
141
|
completed: true,
|
|
137
142
|
model: state.currentModel || undefined,
|
|
138
143
|
provider: state.currentProvider || undefined,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
extractUsage,
|
|
12
12
|
extractCostDetails,
|
|
13
13
|
getCapturePolicy,
|
|
14
|
+
extractModelParameters,
|
|
14
15
|
} from "../utils.js";
|
|
15
16
|
import type { GenerationState, ObservationUpdate } from "../types.js";
|
|
16
17
|
import { applyCapturePolicy } from "../capture-policy.js";
|
|
@@ -39,6 +40,7 @@ export async function startGeneration(event: Record<string, unknown>) {
|
|
|
39
40
|
try {
|
|
40
41
|
const key = getRequestKey(event, `generation-${++state.agentState.generationSeq}`);
|
|
41
42
|
const payload = getProviderPayload(event);
|
|
43
|
+
const modelParameters = extractModelParameters(payload);
|
|
42
44
|
const model = String(event.model ?? event.modelId ?? state.currentModel ?? "");
|
|
43
45
|
const provider = String(event.provider ?? state.currentProvider ?? "");
|
|
44
46
|
const metadata = shapePayload({
|
|
@@ -63,6 +65,7 @@ export async function startGeneration(event: Record<string, unknown>) {
|
|
|
63
65
|
body: {
|
|
64
66
|
input: captured.input,
|
|
65
67
|
model: model || undefined,
|
|
68
|
+
modelParameters,
|
|
66
69
|
metadata: captured.metadata,
|
|
67
70
|
},
|
|
68
71
|
asType: "generation",
|
|
@@ -73,6 +76,7 @@ export async function startGeneration(event: Record<string, unknown>) {
|
|
|
73
76
|
requestKey: key,
|
|
74
77
|
ended: false,
|
|
75
78
|
metadata: captured.metadata ?? {},
|
|
79
|
+
modelParameters,
|
|
76
80
|
});
|
|
77
81
|
state.agentState.generationOrder.push(key);
|
|
78
82
|
} catch (e) {
|
|
@@ -173,10 +177,12 @@ export async function finishGenerationFromMessage(event: Record<string, unknown>
|
|
|
173
177
|
|
|
174
178
|
const usageDetails = extractUsage({ ...event, message });
|
|
175
179
|
const costDetails = extractCostDetails({ ...event, message });
|
|
180
|
+
const modelParameters = extractModelParameters(getProviderPayload(event)) ?? generation.modelParameters;
|
|
176
181
|
const model = String(message.model ?? event.model ?? state.currentModel ?? "");
|
|
177
182
|
const update: ObservationUpdate = {
|
|
178
183
|
output,
|
|
179
184
|
model: model || undefined,
|
|
185
|
+
modelParameters,
|
|
180
186
|
usageDetails,
|
|
181
187
|
costDetails,
|
|
182
188
|
metadata: {
|
|
@@ -202,6 +208,7 @@ export async function createFallbackGenerationFromTurn(event: Record<string, unk
|
|
|
202
208
|
try {
|
|
203
209
|
const usageDetails = extractUsage({ ...event, message });
|
|
204
210
|
const costDetails = extractCostDetails({ ...event, message });
|
|
211
|
+
const modelParameters = extractModelParameters(getProviderPayload(event));
|
|
205
212
|
const model = String(message.model ?? event.model ?? state.currentModel ?? "");
|
|
206
213
|
const captured = applyCapturePolicy(
|
|
207
214
|
{
|
|
@@ -223,6 +230,7 @@ export async function createFallbackGenerationFromTurn(event: Record<string, unk
|
|
|
223
230
|
input: captured.input,
|
|
224
231
|
output: captured.output,
|
|
225
232
|
model: model || undefined,
|
|
233
|
+
modelParameters,
|
|
226
234
|
usageDetails,
|
|
227
235
|
costDetails,
|
|
228
236
|
metadata: captured.metadata,
|
package/src/langfuse.ts
CHANGED
|
@@ -29,6 +29,7 @@ interface RestFallbackObservation {
|
|
|
29
29
|
output?: unknown;
|
|
30
30
|
metadata?: Record<string, unknown>;
|
|
31
31
|
model?: string;
|
|
32
|
+
modelParameters?: Record<string, string | number>;
|
|
32
33
|
usageDetails?: Record<string, number>;
|
|
33
34
|
costDetails?: Record<string, number>;
|
|
34
35
|
level?: "DEBUG" | "DEFAULT" | "WARNING" | "ERROR";
|
|
@@ -114,6 +115,9 @@ function applyObservationUpdate(record: RestFallbackObservation, body: Record<st
|
|
|
114
115
|
record.metadata = mergeMetadata(record.metadata, body.metadata as Record<string, unknown>);
|
|
115
116
|
}
|
|
116
117
|
if (typeof body.model === "string") record.model = body.model;
|
|
118
|
+
if (body.modelParameters && typeof body.modelParameters === "object") {
|
|
119
|
+
record.modelParameters = body.modelParameters as Record<string, string | number>;
|
|
120
|
+
}
|
|
117
121
|
if (body.usageDetails && typeof body.usageDetails === "object") {
|
|
118
122
|
record.usageDetails = body.usageDetails as Record<string, number>;
|
|
119
123
|
}
|
|
@@ -294,6 +298,7 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime) {
|
|
|
294
298
|
? {
|
|
295
299
|
completionStartTime: observation.completionStartTime,
|
|
296
300
|
model: observation.model,
|
|
301
|
+
modelParameters: observation.modelParameters,
|
|
297
302
|
usageDetails: observation.usageDetails,
|
|
298
303
|
costDetails: observation.costDetails,
|
|
299
304
|
}
|
|
@@ -330,7 +335,8 @@ async function fallbackToRestIngestion(rt: LangfuseRuntime) {
|
|
|
330
335
|
}
|
|
331
336
|
|
|
332
337
|
const responseBody = response as { errors?: unknown[] } | undefined;
|
|
333
|
-
const
|
|
338
|
+
const responseErrors = responseBody?.errors;
|
|
339
|
+
const errors = Array.isArray(responseErrors) ? responseErrors : [];
|
|
334
340
|
if (errors.length > 0) {
|
|
335
341
|
console.warn("📊 Langfuse: REST fallback ingestion reported errors", errors);
|
|
336
342
|
} else {
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join } from "node:path";
|
|
3
|
+
import { execFileSync } from "node:child_process";
|
|
4
|
+
|
|
5
|
+
export type SourceMetadata = Record<string, string>;
|
|
6
|
+
|
|
7
|
+
const OVERRIDE_KEYS = new Set([
|
|
8
|
+
"repo_identity",
|
|
9
|
+
"repo_owner",
|
|
10
|
+
"repo_name",
|
|
11
|
+
"source_type",
|
|
12
|
+
"service_name",
|
|
13
|
+
"project_slug",
|
|
14
|
+
"environment",
|
|
15
|
+
"observability_owner",
|
|
16
|
+
]);
|
|
17
|
+
|
|
18
|
+
function nonGitMetadata(): SourceMetadata {
|
|
19
|
+
return {
|
|
20
|
+
source_type: "non-git",
|
|
21
|
+
metadata_source: "non-git",
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runGit(cwd: string, args: string[]): string {
|
|
26
|
+
return execFileSync("git", args, {
|
|
27
|
+
cwd,
|
|
28
|
+
encoding: "utf8",
|
|
29
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
30
|
+
timeout: 2000,
|
|
31
|
+
}).trim();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function firstLine(value: string): string {
|
|
35
|
+
return value.split(/\r?\n/).map((line) => line.trim()).find(Boolean) ?? "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function sanitizeGitRemote(remoteUrl: string): Partial<Pick<SourceMetadata, "git_remote_host" | "git_remote_path">> {
|
|
39
|
+
const raw = firstLine(remoteUrl).replace(/\.git$/i, "");
|
|
40
|
+
if (!raw) {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const parsed = new URL(raw);
|
|
46
|
+
const path = parsed.pathname.replace(/^\/+/, "").replace(/\.git$/i, "");
|
|
47
|
+
return parsed.hostname && path ? { git_remote_host: parsed.hostname, git_remote_path: path } : {};
|
|
48
|
+
} catch {
|
|
49
|
+
// Continue with scp-like SSH syntax, for example git@github.com:owner/repo.git.
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const scpLike = raw.match(/^(?:[^@\s/:]+@)?([^:\s]+):(.+)$/);
|
|
53
|
+
if (scpLike) {
|
|
54
|
+
const host = scpLike[1];
|
|
55
|
+
const path = scpLike[2].replace(/^\/+/, "").replace(/\.git$/i, "");
|
|
56
|
+
return host && path && !path.includes("@") ? { git_remote_host: host, git_remote_path: path } : {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function deriveIdentity(remotePath: string | undefined): Partial<Pick<SourceMetadata, "repo_identity" | "repo_owner" | "repo_name">> {
|
|
63
|
+
if (!remotePath) {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
const parts = remotePath.split("/").filter(Boolean);
|
|
67
|
+
if (parts.length < 2) {
|
|
68
|
+
return {};
|
|
69
|
+
}
|
|
70
|
+
const repoName = parts[parts.length - 1];
|
|
71
|
+
const owner = parts[parts.length - 2];
|
|
72
|
+
if (!repoName || !owner || repoName.includes("/")) {
|
|
73
|
+
return {};
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
repo_identity: `${owner}/${repoName}`,
|
|
77
|
+
repo_owner: owner,
|
|
78
|
+
repo_name: repoName,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function findRepoMetadataFile(cwd: string, gitRoot: string): string | undefined {
|
|
83
|
+
let current = cwd;
|
|
84
|
+
const root = gitRoot;
|
|
85
|
+
while (true) {
|
|
86
|
+
const candidate = join(current, ".pi-langfuse.metadata.json");
|
|
87
|
+
if (existsSync(candidate)) {
|
|
88
|
+
return candidate;
|
|
89
|
+
}
|
|
90
|
+
if (current === root) {
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
const parent = dirname(current);
|
|
94
|
+
if (parent === current) {
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
current = parent;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readWhitelistedOverrides(cwd: string, gitRoot: string): SourceMetadata {
|
|
103
|
+
const path = findRepoMetadataFile(cwd, gitRoot);
|
|
104
|
+
if (!path) {
|
|
105
|
+
return {};
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>;
|
|
109
|
+
const output: SourceMetadata = {};
|
|
110
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
111
|
+
if (OVERRIDE_KEYS.has(key) && typeof value === "string" && value.trim()) {
|
|
112
|
+
output[key] = value.trim();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (output.repo_name?.includes("/")) {
|
|
116
|
+
delete output.repo_name;
|
|
117
|
+
}
|
|
118
|
+
return output;
|
|
119
|
+
} catch {
|
|
120
|
+
return {};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function collectSourceMetadata(cwd: string): SourceMetadata {
|
|
125
|
+
try {
|
|
126
|
+
const inside = runGit(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
127
|
+
if (inside !== "true") {
|
|
128
|
+
return nonGitMetadata();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const gitRoot = runGit(cwd, ["rev-parse", "--show-toplevel"]);
|
|
132
|
+
const commit = runGit(cwd, ["rev-parse", "HEAD"]);
|
|
133
|
+
const branch = runGit(cwd, ["branch", "--show-current"]);
|
|
134
|
+
const remote = runGit(cwd, ["config", "--get", "remote.origin.url"]);
|
|
135
|
+
const remoteMetadata = sanitizeGitRemote(remote);
|
|
136
|
+
const derivedIdentity = deriveIdentity(remoteMetadata.git_remote_path);
|
|
137
|
+
const overrides = readWhitelistedOverrides(cwd, gitRoot);
|
|
138
|
+
|
|
139
|
+
const metadata: SourceMetadata = {
|
|
140
|
+
source_type: "git-repo",
|
|
141
|
+
repo_root_name: basename(gitRoot),
|
|
142
|
+
...(branch ? { git_branch: branch } : {}),
|
|
143
|
+
git_commit: commit,
|
|
144
|
+
...remoteMetadata,
|
|
145
|
+
...derivedIdentity,
|
|
146
|
+
...overrides,
|
|
147
|
+
metadata_source: Object.keys(overrides).length > 0 ? "repo-file" : "git-detection",
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
if (metadata.repo_name?.includes("/")) {
|
|
151
|
+
delete metadata.repo_name;
|
|
152
|
+
}
|
|
153
|
+
if (!metadata.repo_identity && metadata.repo_owner && metadata.repo_name) {
|
|
154
|
+
metadata.repo_identity = `${metadata.repo_owner}/${metadata.repo_name}`;
|
|
155
|
+
}
|
|
156
|
+
return metadata;
|
|
157
|
+
} catch {
|
|
158
|
+
return nonGitMetadata();
|
|
159
|
+
}
|
|
160
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -25,6 +25,7 @@ export interface ObservationUpdate {
|
|
|
25
25
|
output?: unknown;
|
|
26
26
|
metadata?: Record<string, unknown>;
|
|
27
27
|
model?: string;
|
|
28
|
+
modelParameters?: Record<string, string | number>;
|
|
28
29
|
usageDetails?: Record<string, number>;
|
|
29
30
|
usage?: Record<string, number>;
|
|
30
31
|
costDetails?: Record<string, number>;
|
|
@@ -83,6 +84,7 @@ export interface GenerationState {
|
|
|
83
84
|
requestKey: string;
|
|
84
85
|
ended: boolean;
|
|
85
86
|
metadata: Record<string, unknown>;
|
|
87
|
+
modelParameters?: Record<string, string | number>;
|
|
86
88
|
ttftRecorded?: boolean;
|
|
87
89
|
}
|
|
88
90
|
|
|
@@ -105,5 +107,6 @@ export interface AgentState {
|
|
|
105
107
|
generationOrder: string[];
|
|
106
108
|
activeTools: Map<string, ToolState>;
|
|
107
109
|
latestAssistantOutput?: unknown;
|
|
110
|
+
sourceMetadata?: Record<string, unknown>;
|
|
108
111
|
providerMetadataByRequest: Map<string, Record<string, unknown>>;
|
|
109
112
|
}
|
package/src/utils.ts
CHANGED
|
@@ -35,7 +35,7 @@ const PAYLOAD_TOO_LARGE = "[payload too large]";
|
|
|
35
35
|
|
|
36
36
|
export function shapePayload(
|
|
37
37
|
value: unknown,
|
|
38
|
-
options: { maxString?: number; depth?: number; maxNodes?: number; redact?: boolean } = {},
|
|
38
|
+
options: { maxString?: number; depth?: number; maxNodes?: number; redact?: boolean; parseJson?: boolean } = {},
|
|
39
39
|
): unknown {
|
|
40
40
|
const maxString = options.maxString ?? MAX_STRING_LENGTH;
|
|
41
41
|
const depth = options.depth ?? MAX_DEPTH;
|
|
@@ -55,6 +55,9 @@ export function shapePayload(
|
|
|
55
55
|
|
|
56
56
|
if (typeof item === "string") {
|
|
57
57
|
const truncated = truncate(item, maxString);
|
|
58
|
+
if (options.parseJson === false) {
|
|
59
|
+
return truncated;
|
|
60
|
+
}
|
|
58
61
|
const parsed = tryParseJson(truncated);
|
|
59
62
|
if (parsed === truncated) {
|
|
60
63
|
return truncated;
|
|
@@ -175,6 +178,63 @@ export function extractTextContent(content: unknown, maxLength?: number): string
|
|
|
175
178
|
return maxLength ? truncate(text, maxLength) : text;
|
|
176
179
|
}
|
|
177
180
|
|
|
181
|
+
export function normalizeContentForLangfuse(content: unknown, api?: string): unknown {
|
|
182
|
+
if (!Array.isArray(content)) {
|
|
183
|
+
return content;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const toolCallItems = content.filter((item) => {
|
|
187
|
+
return item && typeof item === "object" && (item as { type?: string }).type === "toolCall";
|
|
188
|
+
});
|
|
189
|
+
if (toolCallItems.length === 0) {
|
|
190
|
+
return content;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const text = content
|
|
194
|
+
.map((item) => {
|
|
195
|
+
if (!item || typeof item !== "object") return "";
|
|
196
|
+
const block = item as { type?: string; text?: string };
|
|
197
|
+
return block.type === "text" && typeof block.text === "string" ? block.text : "";
|
|
198
|
+
})
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
.join("");
|
|
201
|
+
|
|
202
|
+
if (api === "anthropic-messages") {
|
|
203
|
+
const blocks: unknown[] = [];
|
|
204
|
+
if (text) {
|
|
205
|
+
blocks.push({ type: "text", text });
|
|
206
|
+
}
|
|
207
|
+
for (const item of toolCallItems) {
|
|
208
|
+
const toolCall = item as { id?: unknown; name?: unknown; arguments?: unknown };
|
|
209
|
+
const toolInput = shapePayload(toolCall.arguments, { parseJson: false });
|
|
210
|
+
blocks.push({
|
|
211
|
+
type: "tool_use",
|
|
212
|
+
id: String(toolCall.id ?? ""),
|
|
213
|
+
name: String(toolCall.name ?? "tool"),
|
|
214
|
+
input: toolInput,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
return blocks;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
role: "assistant",
|
|
222
|
+
content: text || null,
|
|
223
|
+
tool_calls: toolCallItems.map((item) => {
|
|
224
|
+
const toolCall = item as { id?: unknown; name?: unknown; arguments?: unknown };
|
|
225
|
+
const toolArguments = shapePayload(toolCall.arguments ?? {}, { parseJson: false });
|
|
226
|
+
return {
|
|
227
|
+
id: String(toolCall.id ?? ""),
|
|
228
|
+
type: "function",
|
|
229
|
+
function: {
|
|
230
|
+
name: String(toolCall.name ?? "tool"),
|
|
231
|
+
arguments: typeof toolArguments === "string" ? toolArguments : JSON.stringify(toolArguments),
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
178
238
|
export function extractToolCalls(message: Record<string, unknown>): unknown | undefined {
|
|
179
239
|
return (
|
|
180
240
|
message.toolCalls ??
|
|
@@ -182,7 +242,7 @@ export function extractToolCalls(message: Record<string, unknown>): unknown | un
|
|
|
182
242
|
message.function_calls ??
|
|
183
243
|
(message.content && Array.isArray(message.content)
|
|
184
244
|
? message.content.filter((block) => {
|
|
185
|
-
return block && typeof block === "object" && ["tool_use", "tool_call"].includes(String((block as { type?: string }).type));
|
|
245
|
+
return block && typeof block === "object" && ["tool_use", "tool_call", "toolCall"].includes(String((block as { type?: string }).type));
|
|
186
246
|
})
|
|
187
247
|
: undefined)
|
|
188
248
|
);
|
|
@@ -194,6 +254,11 @@ export function extractAssistantOutput(message: unknown): unknown | undefined {
|
|
|
194
254
|
}
|
|
195
255
|
|
|
196
256
|
const msg = message as Record<string, unknown>;
|
|
257
|
+
const normalizedContent = normalizeContentForLangfuse(msg.content, typeof msg.api === "string" ? msg.api : undefined);
|
|
258
|
+
if (normalizedContent !== msg.content) {
|
|
259
|
+
return shapePayload(normalizedContent, { parseJson: false });
|
|
260
|
+
}
|
|
261
|
+
|
|
197
262
|
const text = extractTextContent(msg.content);
|
|
198
263
|
if (text) {
|
|
199
264
|
return text;
|
|
@@ -261,6 +326,33 @@ export function getProviderPayload(event: Record<string, unknown>): unknown {
|
|
|
261
326
|
return event.request ?? event.payload ?? event.body ?? event.providerPayload ?? event.messages ?? event;
|
|
262
327
|
}
|
|
263
328
|
|
|
329
|
+
export function extractModelParameters(payload: unknown): Record<string, string | number> | undefined {
|
|
330
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const params: Record<string, string | number> = {};
|
|
335
|
+
const record = payload as Record<string, unknown>;
|
|
336
|
+
for (const key of [
|
|
337
|
+
"temperature",
|
|
338
|
+
"top_p",
|
|
339
|
+
"topP",
|
|
340
|
+
"max_tokens",
|
|
341
|
+
"maxTokens",
|
|
342
|
+
"max_completion_tokens",
|
|
343
|
+
"presence_penalty",
|
|
344
|
+
"frequency_penalty",
|
|
345
|
+
"reasoning_effort",
|
|
346
|
+
]) {
|
|
347
|
+
const value = record[key];
|
|
348
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
349
|
+
params[key] = value;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return Object.keys(params).length > 0 ? params : undefined;
|
|
354
|
+
}
|
|
355
|
+
|
|
264
356
|
export function getMessageFromEvent(event: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
265
357
|
if (event.message && typeof event.message === "object") {
|
|
266
358
|
return event.message as Record<string, unknown>;
|