@uncoil.dev/sdk 0.0.0-stage → 0.6.0
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 +216 -2
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +88 -0
- package/dist/doctor.d.ts +33 -0
- package/dist/doctor.js +156 -0
- package/dist/index.d.ts +247 -0
- package/dist/index.js +652 -0
- package/dist/integrations/auto.d.ts +16 -0
- package/dist/integrations/auto.js +54 -0
- package/dist/integrations/langgraph.d.ts +24 -0
- package/dist/integrations/langgraph.js +80 -0
- package/dist/integrations/observation.d.ts +31 -0
- package/dist/integrations/observation.js +108 -0
- package/dist/integrations/providers.d.ts +7 -0
- package/dist/integrations/providers.js +224 -0
- package/dist/integrations/tools.d.ts +10 -0
- package/dist/integrations/tools.js +48 -0
- package/dist/integrations/vercel-ai.d.ts +24 -0
- package/dist/integrations/vercel-ai.js +72 -0
- package/dist/login.d.ts +37 -0
- package/dist/login.js +214 -0
- package/package.json +52 -4
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.init = init;
|
|
4
|
+
exports.uninit = uninit;
|
|
5
|
+
const providers_1 = require("./providers");
|
|
6
|
+
const TARGETS = [
|
|
7
|
+
{ provider: "openai", endpoint: "chat.completions.create", module: "openai/resources/chat/completions", exportName: "Completions" },
|
|
8
|
+
{ provider: "openai", endpoint: "responses.create", module: "openai/resources/responses/responses", exportName: "Responses" },
|
|
9
|
+
{ provider: "anthropic", endpoint: "messages.create", module: "@anthropic-ai/sdk/resources/messages", exportName: "Messages" },
|
|
10
|
+
{ provider: "anthropic", endpoint: "beta.messages.create", module: "@anthropic-ai/sdk/resources/beta/messages/messages", exportName: "Messages" },
|
|
11
|
+
];
|
|
12
|
+
const restore = [];
|
|
13
|
+
// A real dynamic import (tsc would turn `import()` into `require` in this CommonJS build).
|
|
14
|
+
const esmImport = new Function("name", "return import(name)");
|
|
15
|
+
function patch(cls, target, options) {
|
|
16
|
+
const original = cls?.prototype?.create;
|
|
17
|
+
if (typeof original !== "function" || original.__uncoil)
|
|
18
|
+
return;
|
|
19
|
+
const patched = function (...args) {
|
|
20
|
+
if ((0, providers_1.isInsideWrapped)())
|
|
21
|
+
return original.apply(this, args);
|
|
22
|
+
return (0, providers_1.traced)(target.provider, target.endpoint, (params) => original.apply(this, [params, ...args.slice(1)]), args[0], options, false);
|
|
23
|
+
};
|
|
24
|
+
patched.__uncoil = true;
|
|
25
|
+
cls.prototype.create = patched;
|
|
26
|
+
restore.push(() => { cls.prototype.create = original; });
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* One-line setup: call `init()` once at startup and every OpenAI and Anthropic call your app makes is traced.
|
|
30
|
+
* It patches the installed `openai` and `@anthropic-ai/sdk` packages (skipping any that are missing), so no
|
|
31
|
+
* client has to be wrapped by hand. Both the CommonJS and the ES-module build are patched; the ES-module one
|
|
32
|
+
* lands a moment later, so `await init().ready` if your very first call happens at once.
|
|
33
|
+
* Call the returned function to switch it off again.
|
|
34
|
+
*
|
|
35
|
+
* For LangChain / LangGraph add `new UncoilLangGraphHandler()` to `callbacks`; for the Vercel AI SDK use
|
|
36
|
+
* `uncoilMiddleware()`. Use these OR `init()` for the same model calls, not both, or they are recorded twice.
|
|
37
|
+
*/
|
|
38
|
+
function init(options = {}) {
|
|
39
|
+
uninit();
|
|
40
|
+
const pending = [];
|
|
41
|
+
for (const target of TARGETS) {
|
|
42
|
+
try {
|
|
43
|
+
patch(require(target.module)[target.exportName], target, options);
|
|
44
|
+
}
|
|
45
|
+
catch { /* package not installed */ }
|
|
46
|
+
pending.push(esmImport(target.module).then((mod) => patch(mod[target.exportName], target, options)).catch(() => { }));
|
|
47
|
+
}
|
|
48
|
+
return Object.assign(() => uninit(), { ready: Promise.all(pending).then(() => undefined) });
|
|
49
|
+
}
|
|
50
|
+
/** Switch off everything `init()` switched on. */
|
|
51
|
+
function uninit() {
|
|
52
|
+
while (restore.length)
|
|
53
|
+
restore.pop()();
|
|
54
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type IntegrationOptions } from "./observation";
|
|
2
|
+
/** Structural LangChain/LangGraph callback handler; no mandatory framework dependency for SDK users. */
|
|
3
|
+
export declare class UncoilLangGraphHandler {
|
|
4
|
+
private options;
|
|
5
|
+
name: string;
|
|
6
|
+
lc_serializable: boolean;
|
|
7
|
+
raiseError: boolean;
|
|
8
|
+
awaitHandlers: boolean;
|
|
9
|
+
private runs;
|
|
10
|
+
constructor(options?: IntegrationOptions);
|
|
11
|
+
private start;
|
|
12
|
+
private end;
|
|
13
|
+
handleChainStart(chain: any, input: unknown, id: string, parent?: string, _tags?: string[], _metadata?: any, _type?: string, name?: string): void;
|
|
14
|
+
handleChainEnd(output: unknown, id: string): void;
|
|
15
|
+
handleChainError(error: unknown, id: string): void;
|
|
16
|
+
handleToolStart(tool: any, input: unknown, id: string, parent?: string, _tags?: string[], _metadata?: any, name?: string): void;
|
|
17
|
+
handleToolEnd(output: unknown, id: string): void;
|
|
18
|
+
handleToolError(error: unknown, id: string): void;
|
|
19
|
+
handleLLMStart(llm: any, input: unknown, id: string, parent?: string, extra?: any): void;
|
|
20
|
+
handleChatModelStart(llm: any, messages: unknown, id: string, parent?: string, extra?: any): void;
|
|
21
|
+
handleLLMNewToken(_token: string, _idx: unknown, id: string): void;
|
|
22
|
+
handleLLMEnd(output: any, id: string): void;
|
|
23
|
+
handleLLMError(error: unknown, id: string): void;
|
|
24
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UncoilLangGraphHandler = void 0;
|
|
4
|
+
const index_1 = require("../index");
|
|
5
|
+
const observation_1 = require("./observation");
|
|
6
|
+
/** Structural LangChain/LangGraph callback handler; no mandatory framework dependency for SDK users. */
|
|
7
|
+
class UncoilLangGraphHandler {
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
this.name = "uncoil";
|
|
11
|
+
this.lc_serializable = false;
|
|
12
|
+
this.raiseError = false;
|
|
13
|
+
this.awaitHandlers = true;
|
|
14
|
+
this.runs = new Map();
|
|
15
|
+
}
|
|
16
|
+
start(type, name, input, id, parent, model = "") {
|
|
17
|
+
if (this.runs.has(id))
|
|
18
|
+
return;
|
|
19
|
+
const ancestor = parent ? this.runs.get(parent) : undefined;
|
|
20
|
+
const context = (0, index_1.currentTraceContext)();
|
|
21
|
+
const outer = !ancestor && context?.runId && context.runId !== "unknown" ? context : undefined;
|
|
22
|
+
this.runs.set(id, (0, observation_1.observation)("langgraph", model, input, this.options, {
|
|
23
|
+
runId: ancestor?.runId ?? outer?.runId ?? id,
|
|
24
|
+
parentSpanId: ancestor?.id ?? outer?.spanId,
|
|
25
|
+
spanId: id,
|
|
26
|
+
root: !ancestor && !outer,
|
|
27
|
+
type,
|
|
28
|
+
name,
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
end(id, output, error) {
|
|
32
|
+
const run = this.runs.get(id);
|
|
33
|
+
if (!run)
|
|
34
|
+
return;
|
|
35
|
+
run.finish(output, error);
|
|
36
|
+
this.runs.delete(id);
|
|
37
|
+
}
|
|
38
|
+
handleChainStart(chain, input, id, parent, _tags, _metadata, _type, name) {
|
|
39
|
+
this.start("agent", name ?? chain?.name ?? chain?.id?.at(-1) ?? "graph", input, id, parent);
|
|
40
|
+
}
|
|
41
|
+
handleChainEnd(output, id) {
|
|
42
|
+
this.end(id, output);
|
|
43
|
+
}
|
|
44
|
+
handleChainError(error, id) {
|
|
45
|
+
this.end(id, undefined, error);
|
|
46
|
+
}
|
|
47
|
+
handleToolStart(tool, input, id, parent, _tags, _metadata, name) {
|
|
48
|
+
this.start("tool_call", name ?? tool?.name ?? tool?.id?.at(-1) ?? "tool", input, id, parent);
|
|
49
|
+
}
|
|
50
|
+
handleToolEnd(output, id) {
|
|
51
|
+
this.end(id, output);
|
|
52
|
+
}
|
|
53
|
+
handleToolError(error, id) {
|
|
54
|
+
this.end(id, undefined, error);
|
|
55
|
+
}
|
|
56
|
+
handleLLMStart(llm, input, id, parent, extra) {
|
|
57
|
+
this.start("llm_call", extra?.invocation_params?.model ?? llm?.name ?? "model", input, id, parent, extra?.invocation_params?.model ?? "");
|
|
58
|
+
}
|
|
59
|
+
handleChatModelStart(llm, messages, id, parent, extra) {
|
|
60
|
+
this.handleLLMStart(llm, messages, id, parent, extra);
|
|
61
|
+
}
|
|
62
|
+
handleLLMNewToken(_token, _idx, id) {
|
|
63
|
+
this.runs.get(id)?.token();
|
|
64
|
+
}
|
|
65
|
+
handleLLMEnd(output, id) {
|
|
66
|
+
const run = this.runs.get(id);
|
|
67
|
+
const generation = output?.generations?.[0]?.[0];
|
|
68
|
+
const usage = output?.llmOutput?.tokenUsage ?? generation?.message?.usage_metadata;
|
|
69
|
+
if (usage)
|
|
70
|
+
run?.usage({
|
|
71
|
+
input_tokens: usage.input_tokens ?? usage.promptTokens,
|
|
72
|
+
output_tokens: usage.output_tokens ?? usage.completionTokens,
|
|
73
|
+
});
|
|
74
|
+
this.end(id, output);
|
|
75
|
+
}
|
|
76
|
+
handleLLMError(error, id) {
|
|
77
|
+
this.end(id, undefined, error);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
exports.UncoilLangGraphHandler = UncoilLangGraphHandler;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { UncoilClient, type SpanData } from "../index";
|
|
2
|
+
export type IntegrationOptions = {
|
|
3
|
+
client?: UncoilClient;
|
|
4
|
+
name?: string;
|
|
5
|
+
captureInput?: boolean;
|
|
6
|
+
captureOutput?: boolean;
|
|
7
|
+
calculateCost?: (usage: {
|
|
8
|
+
provider: string;
|
|
9
|
+
model: string;
|
|
10
|
+
inputTokens: number;
|
|
11
|
+
outputTokens: number;
|
|
12
|
+
usage: Record<string, unknown>;
|
|
13
|
+
}) => number;
|
|
14
|
+
};
|
|
15
|
+
export declare function observation(provider: string, model: string, input: unknown, options: IntegrationOptions, scope?: {
|
|
16
|
+
runId: string;
|
|
17
|
+
parentSpanId?: string;
|
|
18
|
+
spanId?: string;
|
|
19
|
+
root?: boolean;
|
|
20
|
+
type?: string;
|
|
21
|
+
name?: string;
|
|
22
|
+
}): {
|
|
23
|
+
id: string;
|
|
24
|
+
runId: string;
|
|
25
|
+
data: SpanData;
|
|
26
|
+
started: number;
|
|
27
|
+
usage(raw?: Record<string, unknown>, actualModel?: string): void;
|
|
28
|
+
token(): void;
|
|
29
|
+
finish(output?: unknown, error?: unknown, cancelled?: boolean): void;
|
|
30
|
+
};
|
|
31
|
+
export type Observation = ReturnType<typeof observation>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.observation = observation;
|
|
4
|
+
const node_crypto_1 = require("node:crypto");
|
|
5
|
+
const index_1 = require("../index");
|
|
6
|
+
function observation(provider, model, input, options, scope) {
|
|
7
|
+
const context = (0, index_1.currentTraceContext)(), inheritedRun = (0, index_1.currentRunId)();
|
|
8
|
+
const implicit = !scope && inheritedRun === "unknown";
|
|
9
|
+
const runId = scope?.runId ?? (implicit ? (0, node_crypto_1.randomUUID)() : inheritedRun);
|
|
10
|
+
const id = scope?.spanId ?? (0, node_crypto_1.randomUUID)(), rootId = implicit ? (0, node_crypto_1.randomUUID)() : undefined;
|
|
11
|
+
const client = options.client ?? context?.client ?? new index_1.UncoilClient();
|
|
12
|
+
const start = Date.now();
|
|
13
|
+
let ended = false;
|
|
14
|
+
const data = {
|
|
15
|
+
runId,
|
|
16
|
+
spanId: id,
|
|
17
|
+
parentSpanId: scope?.parentSpanId ?? rootId ?? context?.spanId,
|
|
18
|
+
spanType: scope?.type ?? "llm_call",
|
|
19
|
+
toolName: scope?.name ?? `${provider}.${model || "call"}`,
|
|
20
|
+
modelId: model,
|
|
21
|
+
traceName: context?.traceName ?? options.name ?? provider,
|
|
22
|
+
input: options.captureInput === false ? "[not captured]" : input,
|
|
23
|
+
metadata: { provider, costKnown: false },
|
|
24
|
+
createdAt: new Date(start).toISOString(),
|
|
25
|
+
};
|
|
26
|
+
const safeSend = (value) => {
|
|
27
|
+
try {
|
|
28
|
+
void client.ingest(value).catch(() => { });
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
/* Telemetry cannot change application behavior. */
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
return {
|
|
35
|
+
id,
|
|
36
|
+
runId,
|
|
37
|
+
data,
|
|
38
|
+
started: start,
|
|
39
|
+
usage(raw = {}, actualModel = model) {
|
|
40
|
+
data.modelId = actualModel;
|
|
41
|
+
data.inputTokens = Number(raw.input_tokens ?? raw.prompt_tokens ?? 0);
|
|
42
|
+
if (provider === "anthropic")
|
|
43
|
+
data.inputTokens += Number(raw.cache_creation_input_tokens ?? 0) + Number(raw.cache_read_input_tokens ?? 0);
|
|
44
|
+
data.outputTokens = Number(raw.output_tokens ?? raw.completion_tokens ?? 0);
|
|
45
|
+
data.metadata = { ...data.metadata, usage: raw };
|
|
46
|
+
if (options.calculateCost)
|
|
47
|
+
try {
|
|
48
|
+
const cost = options.calculateCost({
|
|
49
|
+
provider,
|
|
50
|
+
model: actualModel,
|
|
51
|
+
inputTokens: data.inputTokens,
|
|
52
|
+
outputTokens: data.outputTokens,
|
|
53
|
+
usage: raw,
|
|
54
|
+
});
|
|
55
|
+
if (Number.isFinite(cost) && cost >= 0) {
|
|
56
|
+
data.costUsd = cost;
|
|
57
|
+
data.metadata.costKnown = true;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
data.metadata.costKnown = false;
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
token() {
|
|
65
|
+
data.ttftMs ?? (data.ttftMs = Date.now() - start);
|
|
66
|
+
},
|
|
67
|
+
finish(output, error, cancelled = false) {
|
|
68
|
+
if (ended)
|
|
69
|
+
return;
|
|
70
|
+
ended = true;
|
|
71
|
+
data.latencyMs = Date.now() - start;
|
|
72
|
+
data.output =
|
|
73
|
+
options.captureOutput === false ? "[not captured]" : (output ?? "");
|
|
74
|
+
if (error !== undefined || cancelled) {
|
|
75
|
+
data.errorType = cancelled
|
|
76
|
+
? "AbortError"
|
|
77
|
+
: error instanceof Error
|
|
78
|
+
? error.name
|
|
79
|
+
: "Error";
|
|
80
|
+
data.errorMessage = (0, index_1.integrationPayload)(cancelled
|
|
81
|
+
? "Stream cancelled before completion"
|
|
82
|
+
: error instanceof Error
|
|
83
|
+
? error.message
|
|
84
|
+
: String(error));
|
|
85
|
+
}
|
|
86
|
+
data.metadata = { ...data.metadata, cancelled };
|
|
87
|
+
if (scope?.root)
|
|
88
|
+
data.spanType = data.errorType ? "error" : "run_end";
|
|
89
|
+
safeSend(data);
|
|
90
|
+
if (implicit)
|
|
91
|
+
safeSend({
|
|
92
|
+
runId,
|
|
93
|
+
spanId: rootId,
|
|
94
|
+
spanType: data.errorType ? "error" : "run_end",
|
|
95
|
+
toolName: options.name ?? provider,
|
|
96
|
+
traceName: data.traceName,
|
|
97
|
+
input: data.input,
|
|
98
|
+
output: data.output,
|
|
99
|
+
latencyMs: data.latencyMs,
|
|
100
|
+
createdAt: data.createdAt,
|
|
101
|
+
metadata: { provider, implicitRoot: true },
|
|
102
|
+
...(data.errorType
|
|
103
|
+
? { errorType: data.errorType, errorMessage: data.errorMessage }
|
|
104
|
+
: {}),
|
|
105
|
+
});
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type IntegrationOptions } from "./observation";
|
|
2
|
+
export type { IntegrationOptions } from "./observation";
|
|
3
|
+
export declare const isInsideWrapped: () => boolean;
|
|
4
|
+
/** Record one provider call. `call` receives the (possibly adjusted) params and returns the provider's result. */
|
|
5
|
+
export declare function traced(provider: string, endpoint: string, call: (params: any) => any, rawParams: any, options: IntegrationOptions, markWrapped?: boolean): any;
|
|
6
|
+
export declare function wrapOpenAI<T extends object>(client: T, options?: IntegrationOptions): T;
|
|
7
|
+
export declare function wrapAnthropic<T extends object>(client: T, options?: IntegrationOptions): T;
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isInsideWrapped = void 0;
|
|
4
|
+
exports.traced = traced;
|
|
5
|
+
exports.wrapOpenAI = wrapOpenAI;
|
|
6
|
+
exports.wrapAnthropic = wrapAnthropic;
|
|
7
|
+
const observation_1 = require("./observation");
|
|
8
|
+
const wrapped = new WeakSet();
|
|
9
|
+
function capture(obs, value) {
|
|
10
|
+
const result = value.response ?? value.message ?? value;
|
|
11
|
+
if (result.usage && value.type !== "message_delta")
|
|
12
|
+
obs.usage(result.usage, result.model ?? obs.data.modelId);
|
|
13
|
+
if (result.model)
|
|
14
|
+
obs.data.modelId = result.model;
|
|
15
|
+
const delta = value.choices?.[0]?.delta?.content ??
|
|
16
|
+
value.delta?.text ??
|
|
17
|
+
(value.type === "response.output_text.delta" ? value.delta : undefined);
|
|
18
|
+
if (typeof delta === "string" && delta)
|
|
19
|
+
obs.token();
|
|
20
|
+
if (value.type === "message_start")
|
|
21
|
+
obs.usage(value.message?.usage, value.message?.model);
|
|
22
|
+
if (value.type === "message_delta" && value.usage)
|
|
23
|
+
obs.usage({ ...(obs.data.metadata?.usage ?? {}), ...Object.fromEntries(Object.entries(value.usage).filter(([, value]) => value != null)) });
|
|
24
|
+
const finishReason = value.choices?.[0]?.finish_reason ??
|
|
25
|
+
value.delta?.stop_reason ??
|
|
26
|
+
result.stop_reason ??
|
|
27
|
+
(value.type === "response.completed" ? "completed" : undefined);
|
|
28
|
+
if (finishReason)
|
|
29
|
+
obs.data.metadata = { ...obs.data.metadata, finishReason };
|
|
30
|
+
if (value.type === "response.failed" || value.type === "error") {
|
|
31
|
+
const e = new Error(value.error?.message ??
|
|
32
|
+
value.response?.error?.message ??
|
|
33
|
+
"Provider stream failed");
|
|
34
|
+
e.name = value.error?.type ?? "ProviderError";
|
|
35
|
+
obs.finish(undefined, e);
|
|
36
|
+
}
|
|
37
|
+
return delta ?? "";
|
|
38
|
+
}
|
|
39
|
+
function observeStream(stream, obs, provider) {
|
|
40
|
+
let text = "";
|
|
41
|
+
const collect = (event) => {
|
|
42
|
+
const delta = capture(obs, event);
|
|
43
|
+
if (typeof delta === "string")
|
|
44
|
+
text = (text + delta).slice(0, 10000);
|
|
45
|
+
};
|
|
46
|
+
// SDK event-emitter helpers consume their own stream. Observe events without iterating it twice.
|
|
47
|
+
if (typeof stream.on === "function") {
|
|
48
|
+
const emitter = stream;
|
|
49
|
+
emitter.on(provider === "anthropic" ? "streamEvent" : "chunk", collect);
|
|
50
|
+
if (provider === "openai")
|
|
51
|
+
emitter.on("event", collect);
|
|
52
|
+
emitter.on("end", () => obs.finish(text));
|
|
53
|
+
emitter.on("error", (error) => obs.finish(text, error));
|
|
54
|
+
emitter.on("abort", () => obs.finish(text, undefined, true));
|
|
55
|
+
return stream;
|
|
56
|
+
}
|
|
57
|
+
return new Proxy(stream, {
|
|
58
|
+
get(target, key) {
|
|
59
|
+
if (key === Symbol.asyncIterator)
|
|
60
|
+
return async function* () {
|
|
61
|
+
let complete = false;
|
|
62
|
+
try {
|
|
63
|
+
for await (const event of target) {
|
|
64
|
+
collect(event);
|
|
65
|
+
yield event;
|
|
66
|
+
}
|
|
67
|
+
complete = true;
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
obs.finish(text, error);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
obs.finish(text, undefined, !complete);
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const value = Reflect.get(target, key, target);
|
|
78
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
/** Preserve APIPromise helpers and stream/controller identity. Do not eagerly parse raw HTTP responses. */
|
|
83
|
+
function observeResult(result, obs, provider) {
|
|
84
|
+
let parsed;
|
|
85
|
+
const consume = (value) => {
|
|
86
|
+
if (value && typeof value[Symbol.asyncIterator] === "function")
|
|
87
|
+
return observeStream(value, obs, provider);
|
|
88
|
+
capture(obs, value ?? {});
|
|
89
|
+
obs.finish(value?.choices ?? value?.output ?? value?.content ?? value);
|
|
90
|
+
return value;
|
|
91
|
+
};
|
|
92
|
+
if (result && typeof result.then === "function")
|
|
93
|
+
return new Proxy(result, {
|
|
94
|
+
get(target, key) {
|
|
95
|
+
const parse = () => (parsed ?? (parsed = Promise.resolve(target)
|
|
96
|
+
.then(consume)
|
|
97
|
+
.catch((error) => {
|
|
98
|
+
obs.finish(undefined, error);
|
|
99
|
+
throw error;
|
|
100
|
+
})));
|
|
101
|
+
if (key === "then")
|
|
102
|
+
return (ok, fail) => parse().then(ok, fail);
|
|
103
|
+
if (key === "catch")
|
|
104
|
+
return (fail) => parse().catch(fail);
|
|
105
|
+
if (key === "finally")
|
|
106
|
+
return (done) => parse().finally(done);
|
|
107
|
+
if (key === "withResponse")
|
|
108
|
+
return async (...args) => {
|
|
109
|
+
try {
|
|
110
|
+
const full = await target.withResponse(...args);
|
|
111
|
+
return { ...full, data: await parse() };
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
obs.finish(undefined, error);
|
|
115
|
+
throw error;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
if (key === "asResponse")
|
|
119
|
+
return async (...args) => {
|
|
120
|
+
try {
|
|
121
|
+
const response = await target.asResponse(...args);
|
|
122
|
+
obs.finish("[raw response not consumed]");
|
|
123
|
+
return response;
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
obs.finish(undefined, error);
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const value = Reflect.get(target, key, target);
|
|
131
|
+
return typeof value === "function" ? value.bind(target) : value;
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
if (result && typeof result[Symbol.asyncIterator] === "function")
|
|
135
|
+
return observeStream(result, obs, provider);
|
|
136
|
+
return consume(result);
|
|
137
|
+
}
|
|
138
|
+
/** True while a wrapped client is calling the provider, so auto-instrumentation does not record the call twice. */
|
|
139
|
+
let insideWrapped = false;
|
|
140
|
+
const isInsideWrapped = () => insideWrapped;
|
|
141
|
+
exports.isInsideWrapped = isInsideWrapped;
|
|
142
|
+
/** Record one provider call. `call` receives the (possibly adjusted) params and returns the provider's result. */
|
|
143
|
+
function traced(provider, endpoint, call, rawParams, options, markWrapped = true) {
|
|
144
|
+
const params = { ...(rawParams ?? {}) };
|
|
145
|
+
if (provider === "openai" &&
|
|
146
|
+
endpoint.startsWith("chat.") &&
|
|
147
|
+
params.stream &&
|
|
148
|
+
params.stream_options?.include_usage === undefined)
|
|
149
|
+
params.stream_options = { ...params.stream_options, include_usage: true };
|
|
150
|
+
const obs = (0, observation_1.observation)(provider, params.model ?? "", params.system
|
|
151
|
+
? [{ role: "system", content: params.system }, ...(params.messages ?? [])]
|
|
152
|
+
: params.instructions
|
|
153
|
+
? { instructions: params.instructions, input: params.input }
|
|
154
|
+
: (params.messages ?? params.input ?? params.prompt ?? ""), options);
|
|
155
|
+
const before = insideWrapped;
|
|
156
|
+
if (markWrapped)
|
|
157
|
+
insideWrapped = true;
|
|
158
|
+
try {
|
|
159
|
+
return observeResult(call(params), obs, provider);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
obs.finish(undefined, error);
|
|
163
|
+
throw error;
|
|
164
|
+
}
|
|
165
|
+
finally {
|
|
166
|
+
insideWrapped = before;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function wrap(client, provider, options) {
|
|
170
|
+
if (wrapped.has(client))
|
|
171
|
+
return client;
|
|
172
|
+
const cache = new WeakMap();
|
|
173
|
+
const proxy = (target, path) => {
|
|
174
|
+
const existing = cache.get(target);
|
|
175
|
+
if (existing)
|
|
176
|
+
return existing;
|
|
177
|
+
const result = new Proxy(target, {
|
|
178
|
+
get(obj, key) {
|
|
179
|
+
const value = Reflect.get(obj, key, obj);
|
|
180
|
+
const next = [...path, String(key)];
|
|
181
|
+
const endpoint = next.join(".");
|
|
182
|
+
const allowed = provider === "openai"
|
|
183
|
+
? [
|
|
184
|
+
"chat.completions.create",
|
|
185
|
+
"chat.completions.stream",
|
|
186
|
+
"responses.create",
|
|
187
|
+
"responses.stream",
|
|
188
|
+
]
|
|
189
|
+
: [
|
|
190
|
+
"messages.create",
|
|
191
|
+
"messages.stream",
|
|
192
|
+
"beta.messages.create",
|
|
193
|
+
"beta.messages.stream",
|
|
194
|
+
];
|
|
195
|
+
if (typeof value === "function" && allowed.includes(endpoint))
|
|
196
|
+
return (...args) => traced(provider, endpoint, (params) => value.apply(obj, [params, ...args.slice(1)]), args[0], options);
|
|
197
|
+
if (value &&
|
|
198
|
+
typeof value === "object" &&
|
|
199
|
+
(path.length === 0 ||
|
|
200
|
+
[
|
|
201
|
+
"chat",
|
|
202
|
+
"chat.completions",
|
|
203
|
+
"responses",
|
|
204
|
+
"messages",
|
|
205
|
+
"beta",
|
|
206
|
+
"beta.messages",
|
|
207
|
+
].includes(next.join("."))))
|
|
208
|
+
return proxy(value, next);
|
|
209
|
+
return typeof value === "function" ? value.bind(obj) : value;
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
cache.set(target, result);
|
|
213
|
+
return result;
|
|
214
|
+
};
|
|
215
|
+
const result = proxy(client, []);
|
|
216
|
+
wrapped.add(result);
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
function wrapOpenAI(client, options = {}) {
|
|
220
|
+
return wrap(client, "openai", options);
|
|
221
|
+
}
|
|
222
|
+
function wrapAnthropic(client, options = {}) {
|
|
223
|
+
return wrap(client, "anthropic", options);
|
|
224
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type IntegrationOptions } from "./observation";
|
|
2
|
+
/**
|
|
3
|
+
* Trace any set of tools. Works with plain functions and with tool objects that carry an `execute`
|
|
4
|
+
* function (the Vercel AI SDK, OpenAI Agents and most agent frameworks use that shape):
|
|
5
|
+
*
|
|
6
|
+
* const tools = traceTools({ searchDocs, sendEmail });
|
|
7
|
+
*
|
|
8
|
+
* The returned object has the same keys and behaves the same; each call is recorded as a tool step.
|
|
9
|
+
*/
|
|
10
|
+
export declare function traceTools<T extends Record<string, any>>(tools: T, options?: IntegrationOptions): T;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.traceTools = traceTools;
|
|
4
|
+
const observation_1 = require("./observation");
|
|
5
|
+
/**
|
|
6
|
+
* Trace any set of tools. Works with plain functions and with tool objects that carry an `execute`
|
|
7
|
+
* function (the Vercel AI SDK, OpenAI Agents and most agent frameworks use that shape):
|
|
8
|
+
*
|
|
9
|
+
* const tools = traceTools({ searchDocs, sendEmail });
|
|
10
|
+
*
|
|
11
|
+
* The returned object has the same keys and behaves the same; each call is recorded as a tool step.
|
|
12
|
+
*/
|
|
13
|
+
function traceTools(tools, options = {}) {
|
|
14
|
+
const traced = {};
|
|
15
|
+
for (const [name, tool] of Object.entries(tools)) {
|
|
16
|
+
if (typeof tool === "function")
|
|
17
|
+
traced[name] = wrapTool(name, tool, options);
|
|
18
|
+
else if (tool && typeof tool.execute === "function") {
|
|
19
|
+
const copy = Object.create(Object.getPrototypeOf(tool), Object.getOwnPropertyDescriptors(tool));
|
|
20
|
+
copy.execute = wrapTool(name, tool.execute.bind(tool), options);
|
|
21
|
+
traced[name] = copy;
|
|
22
|
+
}
|
|
23
|
+
else
|
|
24
|
+
traced[name] = tool;
|
|
25
|
+
}
|
|
26
|
+
return traced;
|
|
27
|
+
}
|
|
28
|
+
function wrapTool(name, fn, options) {
|
|
29
|
+
return function (...args) {
|
|
30
|
+
const obs = (0, observation_1.observation)("tool", name, args.length > 1 ? args : args[0], options);
|
|
31
|
+
obs.data.spanType = "tool_call";
|
|
32
|
+
obs.data.toolName = name;
|
|
33
|
+
obs.data.modelId = undefined;
|
|
34
|
+
let outcome;
|
|
35
|
+
try {
|
|
36
|
+
outcome = fn.apply(this, args);
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
obs.finish(undefined, error);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
if (outcome && typeof outcome.then === "function") {
|
|
43
|
+
return outcome.then((value) => { obs.finish(value); return value; }, (error) => { obs.finish(undefined, error); throw error; });
|
|
44
|
+
}
|
|
45
|
+
obs.finish(outcome);
|
|
46
|
+
return outcome;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type IntegrationOptions } from "./observation";
|
|
2
|
+
type Value = Record<string, any>;
|
|
3
|
+
/**
|
|
4
|
+
* One-line tracing for the Vercel AI SDK. Pass it to `wrapLanguageModel`:
|
|
5
|
+
*
|
|
6
|
+
* const model = wrapLanguageModel({ model: openai("gpt-4o"), middleware: uncoilMiddleware() });
|
|
7
|
+
*
|
|
8
|
+
* Every `generateText`, `streamText` or `generateObject` call made with that model becomes an Uncoil step.
|
|
9
|
+
*/
|
|
10
|
+
export declare function uncoilMiddleware(options?: IntegrationOptions): {
|
|
11
|
+
wrapGenerate({ doGenerate, params, model }: {
|
|
12
|
+
doGenerate: () => Promise<Value>;
|
|
13
|
+
params: unknown;
|
|
14
|
+
model?: Value;
|
|
15
|
+
}): Promise<Value>;
|
|
16
|
+
wrapStream({ doStream, params, model }: {
|
|
17
|
+
doStream: () => Promise<Value>;
|
|
18
|
+
params: unknown;
|
|
19
|
+
model?: Value;
|
|
20
|
+
}): Promise<{
|
|
21
|
+
stream: any;
|
|
22
|
+
}>;
|
|
23
|
+
};
|
|
24
|
+
export {};
|