@shpitdev/convex-cliproxy-observability 0.1.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/CHANGELOG.md +11 -0
- package/LICENSE +201 -0
- package/README.md +40 -0
- package/dist/capture/index.d.ts +79 -0
- package/dist/capture/index.d.ts.map +1 -0
- package/dist/capture/index.js +182 -0
- package/dist/capture/index.js.map +1 -0
- package/dist/client.d.ts +129 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +232 -0
- package/dist/client.js.map +1 -0
- package/dist/component/_generated/api.d.ts +12 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +7 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +17 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +3 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +8 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +3 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +21 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +11 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +4 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +4 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/ingest.d.ts +64 -0
- package/dist/component/ingest.d.ts.map +1 -0
- package/dist/component/ingest.js +309 -0
- package/dist/component/ingest.js.map +1 -0
- package/dist/component/queries.d.ts +136 -0
- package/dist/component/queries.d.ts.map +1 -0
- package/dist/component/queries.js +219 -0
- package/dist/component/queries.js.map +1 -0
- package/dist/component/schema.d.ts +159 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +74 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/content/index.d.ts +8 -0
- package/dist/content/index.d.ts.map +1 -0
- package/dist/content/index.js +41 -0
- package/dist/content/index.js.map +1 -0
- package/dist/model-call/index.d.ts +120 -0
- package/dist/model-call/index.d.ts.map +1 -0
- package/dist/model-call/index.js +96 -0
- package/dist/model-call/index.js.map +1 -0
- package/dist/projection.d.ts +19 -0
- package/dist/projection.d.ts.map +1 -0
- package/dist/projection.js +83 -0
- package/dist/projection.js.map +1 -0
- package/dist/protocols/anthropic-messages.d.ts +11 -0
- package/dist/protocols/anthropic-messages.d.ts.map +1 -0
- package/dist/protocols/anthropic-messages.js +114 -0
- package/dist/protocols/anthropic-messages.js.map +1 -0
- package/dist/protocols/chat-completions.d.ts +10 -0
- package/dist/protocols/chat-completions.d.ts.map +1 -0
- package/dist/protocols/chat-completions.js +89 -0
- package/dist/protocols/chat-completions.js.map +1 -0
- package/dist/protocols/checkpoint.d.ts +20 -0
- package/dist/protocols/checkpoint.d.ts.map +1 -0
- package/dist/protocols/checkpoint.js +318 -0
- package/dist/protocols/checkpoint.js.map +1 -0
- package/dist/protocols/content.d.ts +34 -0
- package/dist/protocols/content.d.ts.map +1 -0
- package/dist/protocols/content.js +40 -0
- package/dist/protocols/content.js.map +1 -0
- package/dist/protocols/framing.d.ts +20 -0
- package/dist/protocols/framing.d.ts.map +1 -0
- package/dist/protocols/framing.js +67 -0
- package/dist/protocols/framing.js.map +1 -0
- package/dist/protocols/index.d.ts +20 -0
- package/dist/protocols/index.d.ts.map +1 -0
- package/dist/protocols/index.js +139 -0
- package/dist/protocols/index.js.map +1 -0
- package/dist/protocols/output.d.ts +4 -0
- package/dist/protocols/output.d.ts.map +1 -0
- package/dist/protocols/output.js +81 -0
- package/dist/protocols/output.js.map +1 -0
- package/dist/protocols/responses.d.ts +12 -0
- package/dist/protocols/responses.d.ts.map +1 -0
- package/dist/protocols/responses.js +189 -0
- package/dist/protocols/responses.js.map +1 -0
- package/dist/protocols/types.d.ts +27 -0
- package/dist/protocols/types.d.ts.map +1 -0
- package/dist/protocols/types.js +2 -0
- package/dist/protocols/types.js.map +1 -0
- package/dist/protocols/usage.d.ts +3 -0
- package/dist/protocols/usage.d.ts.map +1 -0
- package/dist/protocols/usage.js +27 -0
- package/dist/protocols/usage.js.map +1 -0
- package/dist/protocols/values.d.ts +10 -0
- package/dist/protocols/values.d.ts.map +1 -0
- package/dist/protocols/values.js +32 -0
- package/dist/protocols/values.js.map +1 -0
- package/dist/test.d.ts +155 -0
- package/dist/test.d.ts.map +1 -0
- package/dist/test.js +8 -0
- package/dist/test.js.map +1 -0
- package/package.json +90 -0
- package/src/capture/index.ts +277 -0
- package/src/client.ts +286 -0
- package/src/component/_generated/api.ts +19 -0
- package/src/component/_generated/component.ts +32 -0
- package/src/component/_generated/dataModel.ts +15 -0
- package/src/component/_generated/server.ts +43 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/ingest.ts +328 -0
- package/src/component/queries.ts +248 -0
- package/src/component/schema.ts +73 -0
- package/src/content/index.ts +53 -0
- package/src/model-call/index.ts +224 -0
- package/src/projection.ts +99 -0
- package/src/protocols/anthropic-messages.ts +115 -0
- package/src/protocols/chat-completions.ts +94 -0
- package/src/protocols/checkpoint.ts +342 -0
- package/src/protocols/content.ts +41 -0
- package/src/protocols/framing.ts +62 -0
- package/src/protocols/index.ts +140 -0
- package/src/protocols/output.ts +78 -0
- package/src/protocols/responses.ts +188 -0
- package/src/protocols/types.ts +26 -0
- package/src/protocols/usage.ts +36 -0
- package/src/protocols/values.ts +35 -0
- package/src/test.ts +16 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/** Read contract only: storage and ingestion remain independently owned. */
|
|
2
|
+
export type ModelCallSource = "cliproxy" | "openrouter" | "cliproxy_legacy";
|
|
3
|
+
export type ModelCallState =
|
|
4
|
+
"in_progress" | "succeeded" | "failed" | "aborted" | "rejected" | "interrupted" | "unknown";
|
|
5
|
+
export type EvidenceState = "complete" | "partial" | "unavailable" | "invalid" | "unsupported";
|
|
6
|
+
export type ExactCorrelation = Partial<
|
|
7
|
+
Record<
|
|
8
|
+
| "requestId"
|
|
9
|
+
| "runId"
|
|
10
|
+
| "jobId"
|
|
11
|
+
| "traceId"
|
|
12
|
+
| "rootExecutionId"
|
|
13
|
+
| "opencodeSessionId"
|
|
14
|
+
| "operationId"
|
|
15
|
+
| "stepId"
|
|
16
|
+
| "partId"
|
|
17
|
+
| "attemptId",
|
|
18
|
+
string
|
|
19
|
+
>
|
|
20
|
+
>;
|
|
21
|
+
export type UsageMeasurement = {
|
|
22
|
+
value: number;
|
|
23
|
+
nativeField: string;
|
|
24
|
+
source: "client_protocol" | "openrouter_span";
|
|
25
|
+
scope: "call";
|
|
26
|
+
unit: "tokens";
|
|
27
|
+
finality: "final" | "partial" | "unknown";
|
|
28
|
+
semanticsVersion: string;
|
|
29
|
+
};
|
|
30
|
+
export type PrivateContentReference = {
|
|
31
|
+
key: string;
|
|
32
|
+
sha256: string;
|
|
33
|
+
byteLength: number;
|
|
34
|
+
contentType: string;
|
|
35
|
+
};
|
|
36
|
+
export type ModelCallV1 = {
|
|
37
|
+
schemaVersion: 1;
|
|
38
|
+
source: ModelCallSource;
|
|
39
|
+
callId: string;
|
|
40
|
+
sourceDocumentId: string;
|
|
41
|
+
gateway: "cliproxy" | "openrouter";
|
|
42
|
+
deploymentId?: string;
|
|
43
|
+
environment?: "dev" | "prod";
|
|
44
|
+
route?: string;
|
|
45
|
+
configRevision?: string;
|
|
46
|
+
authType: "unknown";
|
|
47
|
+
destinationId?: string;
|
|
48
|
+
instanceId?: string;
|
|
49
|
+
pluginBootId?: string;
|
|
50
|
+
executionId?: string;
|
|
51
|
+
sourceTraceId?: string;
|
|
52
|
+
traceId?: string;
|
|
53
|
+
spanId?: string;
|
|
54
|
+
parentSpanId?: string;
|
|
55
|
+
requestModel?: string;
|
|
56
|
+
responseModel?: string;
|
|
57
|
+
providerName?: string;
|
|
58
|
+
streamed?: boolean;
|
|
59
|
+
operation: "generation" | "token_count" | "discovery" | "unknown";
|
|
60
|
+
tokenCount?: number;
|
|
61
|
+
clientProtocol?: "anthropic_messages" | "responses" | "chat_completions" | "unknown";
|
|
62
|
+
state: ModelCallState;
|
|
63
|
+
completionOutcome?: string;
|
|
64
|
+
executionStatusCode?: number;
|
|
65
|
+
protocolTerminal?: string;
|
|
66
|
+
finishReason?: string;
|
|
67
|
+
startTimeUnixNano?: string;
|
|
68
|
+
endTimeUnixNano?: string;
|
|
69
|
+
latencyMs?: number;
|
|
70
|
+
timeToFirstByteMs?: number;
|
|
71
|
+
timeToFirstContentMs?: number;
|
|
72
|
+
timingSource: "plugin_observed" | "openrouter_span" | "unavailable";
|
|
73
|
+
correlation: ExactCorrelation;
|
|
74
|
+
correlationConflicts: string[];
|
|
75
|
+
inputTokens?: number;
|
|
76
|
+
outputTokens?: number;
|
|
77
|
+
totalTokens?: number;
|
|
78
|
+
reasoningTokens?: number;
|
|
79
|
+
cachedInputTokens?: number;
|
|
80
|
+
usage: UsageMeasurement[];
|
|
81
|
+
cost:
|
|
82
|
+
| { kind: "unknown" }
|
|
83
|
+
| { kind: "provider_billed" | "proxy_reported"; currency: string; total: string };
|
|
84
|
+
attemptDetail: "unavailable";
|
|
85
|
+
capture: {
|
|
86
|
+
raw: EvidenceState;
|
|
87
|
+
projection: EvidenceState;
|
|
88
|
+
usage: EvidenceState;
|
|
89
|
+
preHook: "unavailable";
|
|
90
|
+
projectedThroughSequence: number;
|
|
91
|
+
persistedThroughSequence: number;
|
|
92
|
+
terminalSequence?: number;
|
|
93
|
+
gaps: string[];
|
|
94
|
+
parserVersion: string;
|
|
95
|
+
pluginVersion?: string;
|
|
96
|
+
redactionVersion?: string;
|
|
97
|
+
lastObservedAt?: string;
|
|
98
|
+
};
|
|
99
|
+
receivedAt: number;
|
|
100
|
+
};
|
|
101
|
+
export type ModelCallSummaryV1 = ModelCallV1;
|
|
102
|
+
|
|
103
|
+
/** Structural input: there is no runtime dependency on the OpenRouter component. */
|
|
104
|
+
export type OpenRouterSpanInput = {
|
|
105
|
+
_id: string;
|
|
106
|
+
receivedAt: number;
|
|
107
|
+
totalCost?: number;
|
|
108
|
+
openrouterUsageCost?: number;
|
|
109
|
+
traceId?: string;
|
|
110
|
+
spanId?: string;
|
|
111
|
+
parentSpanId?: string;
|
|
112
|
+
requestModel?: string;
|
|
113
|
+
responseModel?: string;
|
|
114
|
+
providerName?: string;
|
|
115
|
+
streamed?: boolean;
|
|
116
|
+
startTimeUnixNano?: string;
|
|
117
|
+
endTimeUnixNano?: string;
|
|
118
|
+
finishReason?: string;
|
|
119
|
+
requestId?: string;
|
|
120
|
+
runId?: string;
|
|
121
|
+
jobId?: string;
|
|
122
|
+
rootExecutionId?: string;
|
|
123
|
+
opencodeSessionId?: string;
|
|
124
|
+
inputTokens?: number;
|
|
125
|
+
outputTokens?: number;
|
|
126
|
+
totalTokens?: number;
|
|
127
|
+
reasoningTokens?: number;
|
|
128
|
+
cachedInputTokens?: number;
|
|
129
|
+
};
|
|
130
|
+
export function fromOpenRouterSpan(span: OpenRouterSpanInput): ModelCallV1 {
|
|
131
|
+
const correlation: ExactCorrelation = {};
|
|
132
|
+
for (const field of [
|
|
133
|
+
"requestId",
|
|
134
|
+
"runId",
|
|
135
|
+
"jobId",
|
|
136
|
+
"rootExecutionId",
|
|
137
|
+
"opencodeSessionId",
|
|
138
|
+
] as const) {
|
|
139
|
+
if (span[field] !== undefined) correlation[field] = span[field];
|
|
140
|
+
}
|
|
141
|
+
const usage: UsageMeasurement[] = [];
|
|
142
|
+
for (const field of [
|
|
143
|
+
"inputTokens",
|
|
144
|
+
"outputTokens",
|
|
145
|
+
"totalTokens",
|
|
146
|
+
"reasoningTokens",
|
|
147
|
+
"cachedInputTokens",
|
|
148
|
+
] as const) {
|
|
149
|
+
const value = span[field];
|
|
150
|
+
if (value !== undefined && Number.isSafeInteger(value) && value >= 0)
|
|
151
|
+
usage.push({
|
|
152
|
+
value,
|
|
153
|
+
nativeField: field,
|
|
154
|
+
source: "openrouter_span",
|
|
155
|
+
scope: "call",
|
|
156
|
+
unit: "tokens",
|
|
157
|
+
finality: "unknown",
|
|
158
|
+
semanticsVersion: "stored_openrouter_span_v1",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
schemaVersion: 1,
|
|
163
|
+
source: "openrouter",
|
|
164
|
+
callId: `openrouter:${span._id}`,
|
|
165
|
+
sourceDocumentId: span._id,
|
|
166
|
+
gateway: "openrouter",
|
|
167
|
+
authType: "unknown",
|
|
168
|
+
traceId: span.traceId,
|
|
169
|
+
spanId: span.spanId,
|
|
170
|
+
parentSpanId: span.parentSpanId,
|
|
171
|
+
requestModel: span.requestModel,
|
|
172
|
+
responseModel: span.responseModel,
|
|
173
|
+
providerName: span.providerName,
|
|
174
|
+
streamed: span.streamed,
|
|
175
|
+
operation: "generation",
|
|
176
|
+
state: "unknown",
|
|
177
|
+
finishReason: span.finishReason,
|
|
178
|
+
startTimeUnixNano: span.startTimeUnixNano,
|
|
179
|
+
endTimeUnixNano: span.endTimeUnixNano,
|
|
180
|
+
timingSource: "openrouter_span",
|
|
181
|
+
correlation,
|
|
182
|
+
correlationConflicts: [],
|
|
183
|
+
inputTokens: span.inputTokens,
|
|
184
|
+
outputTokens: span.outputTokens,
|
|
185
|
+
totalTokens: span.totalTokens,
|
|
186
|
+
reasoningTokens: span.reasoningTokens,
|
|
187
|
+
cachedInputTokens: span.cachedInputTokens,
|
|
188
|
+
usage,
|
|
189
|
+
cost:
|
|
190
|
+
typeof (span.totalCost ?? span.openrouterUsageCost) === "number" &&
|
|
191
|
+
Number.isFinite(span.totalCost ?? span.openrouterUsageCost) &&
|
|
192
|
+
(span.totalCost ?? span.openrouterUsageCost)! >= 0
|
|
193
|
+
? {
|
|
194
|
+
kind: "proxy_reported",
|
|
195
|
+
currency: "USD",
|
|
196
|
+
total: String(span.totalCost ?? span.openrouterUsageCost),
|
|
197
|
+
}
|
|
198
|
+
: { kind: "unknown" },
|
|
199
|
+
attemptDetail: "unavailable",
|
|
200
|
+
capture: {
|
|
201
|
+
raw: "unavailable",
|
|
202
|
+
projection: "partial",
|
|
203
|
+
usage: usage.length ? "partial" : "unavailable",
|
|
204
|
+
preHook: "unavailable",
|
|
205
|
+
projectedThroughSequence: 0,
|
|
206
|
+
persistedThroughSequence: 0,
|
|
207
|
+
gaps: ["stream_events_and_attempt_detail_unavailable"],
|
|
208
|
+
parserVersion: "openrouter-structural-v1",
|
|
209
|
+
},
|
|
210
|
+
receivedAt: span.receivedAt,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
export function fromLegacyCliproxySpan(span: OpenRouterSpanInput): ModelCallV1 {
|
|
214
|
+
const call = fromOpenRouterSpan(span);
|
|
215
|
+
return {
|
|
216
|
+
...call,
|
|
217
|
+
source: "cliproxy_legacy",
|
|
218
|
+
callId: `cliproxy_legacy:${span._id}`,
|
|
219
|
+
gateway: "cliproxy",
|
|
220
|
+
providerName: undefined,
|
|
221
|
+
timingSource: "unavailable",
|
|
222
|
+
cost: { kind: "unknown" },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { GenericActionCtx, GenericDataModel } from "convex/server";
|
|
2
|
+
import type { CliproxyObservability } from "./client.js";
|
|
3
|
+
import { resolveCallBlob, type PrivateCaptureStorage } from "./content/index.js";
|
|
4
|
+
import { sha256, type CaptureObservationV1 } from "./capture/index.js";
|
|
5
|
+
import { applyObservation, type ProjectionCheckpoint } from "./protocols/checkpoint.js";
|
|
6
|
+
import type { ModelCallV1 } from "./model-call/index.js";
|
|
7
|
+
/** Project at most two immutable segments. Host schedules the next page when `more` is true. */
|
|
8
|
+
export async function projectPendingSegments(
|
|
9
|
+
ctx: Pick<GenericActionCtx<GenericDataModel>, "runQuery" | "runMutation">,
|
|
10
|
+
options: {
|
|
11
|
+
client: CliproxyObservability;
|
|
12
|
+
storage: PrivateCaptureStorage;
|
|
13
|
+
destinationId: string;
|
|
14
|
+
callId: string;
|
|
15
|
+
},
|
|
16
|
+
) {
|
|
17
|
+
const { client, destinationId, callId } = options;
|
|
18
|
+
const state = await ctx.runQuery(client.component.queries.getProcessingState, {
|
|
19
|
+
destinationId,
|
|
20
|
+
callId,
|
|
21
|
+
});
|
|
22
|
+
if (!state) return { progressed: false, more: false };
|
|
23
|
+
try {
|
|
24
|
+
const page = await ctx.runQuery(client.component.queries.pageEventSegments, {
|
|
25
|
+
destinationId,
|
|
26
|
+
callId,
|
|
27
|
+
afterSequence: state.projectedThroughSequence,
|
|
28
|
+
limit: 2,
|
|
29
|
+
});
|
|
30
|
+
const call = JSON.parse(state.summaryJson) as ModelCallV1;
|
|
31
|
+
call.capture.projectedThroughSequence = state.projectedThroughSequence;
|
|
32
|
+
const checkpoint = JSON.parse(state.checkpointJson) as ProjectionCheckpoint;
|
|
33
|
+
if (checkpoint.sseBlob) {
|
|
34
|
+
const stored = await resolveCallBlob(
|
|
35
|
+
options.storage,
|
|
36
|
+
[checkpoint.sseBlob],
|
|
37
|
+
checkpoint.sseBlob,
|
|
38
|
+
);
|
|
39
|
+
checkpoint.sse = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(stored));
|
|
40
|
+
delete checkpoint.sseBlob;
|
|
41
|
+
}
|
|
42
|
+
let through = state.projectedThroughSequence;
|
|
43
|
+
let waitingForGap = false;
|
|
44
|
+
for (const segment of page.segments) {
|
|
45
|
+
if (segment.sequence !== through + 1) {
|
|
46
|
+
waitingForGap = true;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
const content = await resolveCallBlob(
|
|
50
|
+
options.storage,
|
|
51
|
+
[segment.reference],
|
|
52
|
+
segment.reference,
|
|
53
|
+
);
|
|
54
|
+
for (const line of new TextDecoder("utf-8", { fatal: true })
|
|
55
|
+
.decode(content)
|
|
56
|
+
.split("\n")
|
|
57
|
+
.filter(Boolean))
|
|
58
|
+
applyObservation(call, checkpoint, JSON.parse(line) as CaptureObservationV1);
|
|
59
|
+
through = segment.throughSequence;
|
|
60
|
+
}
|
|
61
|
+
if (through === state.projectedThroughSequence)
|
|
62
|
+
return { progressed: false, more: false, waitingForGap };
|
|
63
|
+
// A frame can exceed Convex's checkpoint budget. Persist pending framing bytes
|
|
64
|
+
// privately; the bounded component checkpoint owns the exact reference.
|
|
65
|
+
if (checkpoint.sse && checkpoint.sse.pendingBase64.length > 16 * 1024) {
|
|
66
|
+
const bytes = new TextEncoder().encode(JSON.stringify(checkpoint.sse));
|
|
67
|
+
const digest = await sha256(bytes);
|
|
68
|
+
const ref = {
|
|
69
|
+
key: `observability/cliproxy/v2/${destinationId}/${state.instanceId}/${callId}/parser/${digest}.json`,
|
|
70
|
+
sha256: digest,
|
|
71
|
+
byteLength: bytes.length,
|
|
72
|
+
contentType: "application/json",
|
|
73
|
+
};
|
|
74
|
+
await options.storage.put(ref, bytes);
|
|
75
|
+
checkpoint.sseBlob = ref;
|
|
76
|
+
delete checkpoint.sse;
|
|
77
|
+
}
|
|
78
|
+
const committed = await ctx.runMutation(client.component.ingest.commitProjection, {
|
|
79
|
+
destinationId,
|
|
80
|
+
callId,
|
|
81
|
+
expectedRevision: state.revision,
|
|
82
|
+
throughSequence: through,
|
|
83
|
+
summaryJson: JSON.stringify(call),
|
|
84
|
+
checkpointJson: JSON.stringify(checkpoint),
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
progressed: committed.committed,
|
|
88
|
+
more: !committed.committed || (!page.done && !waitingForGap),
|
|
89
|
+
waitingForGap,
|
|
90
|
+
};
|
|
91
|
+
} catch (error) {
|
|
92
|
+
await ctx.runMutation(client.component.ingest.recordProjectionFailure, {
|
|
93
|
+
destinationId,
|
|
94
|
+
callId,
|
|
95
|
+
expectedRevision: state.revision,
|
|
96
|
+
});
|
|
97
|
+
throw error;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { RecordValue } from "./types.js";
|
|
2
|
+
import { append, count, list, object } from "./values.js";
|
|
3
|
+
export function anthropicMessages(messages: unknown): RecordValue[] {
|
|
4
|
+
const result: RecordValue[] = [];
|
|
5
|
+
for (const value of list(messages)) {
|
|
6
|
+
const message = object(value);
|
|
7
|
+
if (!Array.isArray(message.content)) {
|
|
8
|
+
result.push(message);
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
let content: unknown[] = [];
|
|
12
|
+
let tools: unknown[] = [];
|
|
13
|
+
const flush = () => {
|
|
14
|
+
if (content.length || tools.length)
|
|
15
|
+
result.push({
|
|
16
|
+
role: message.role,
|
|
17
|
+
content,
|
|
18
|
+
...(tools.length ? { tool_calls: tools } : {}),
|
|
19
|
+
});
|
|
20
|
+
content = [];
|
|
21
|
+
tools = [];
|
|
22
|
+
};
|
|
23
|
+
for (const raw of message.content) {
|
|
24
|
+
const block = object(raw);
|
|
25
|
+
if (block.type === "tool_use") {
|
|
26
|
+
tools.push({
|
|
27
|
+
id: block.id,
|
|
28
|
+
type: "function",
|
|
29
|
+
function: {
|
|
30
|
+
name: block.name,
|
|
31
|
+
arguments:
|
|
32
|
+
typeof block.partial_json === "string"
|
|
33
|
+
? block.partial_json
|
|
34
|
+
: JSON.stringify(block.input),
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
} else if (block.type === "tool_result") {
|
|
38
|
+
flush();
|
|
39
|
+
result.push({
|
|
40
|
+
role: "tool",
|
|
41
|
+
tool_call_id: block.tool_use_id,
|
|
42
|
+
content: block.content,
|
|
43
|
+
is_error: block.is_error,
|
|
44
|
+
});
|
|
45
|
+
} else if (block.type === "text") content.push({ type: "text", text: block.text });
|
|
46
|
+
else content.push(block);
|
|
47
|
+
}
|
|
48
|
+
flush();
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function assembleAnthropicStream(events: RecordValue[]) {
|
|
54
|
+
let response: RecordValue = {};
|
|
55
|
+
let invalid = false,
|
|
56
|
+
recognized = false,
|
|
57
|
+
unsupported = false,
|
|
58
|
+
terminal = false,
|
|
59
|
+
terminalUsage = false;
|
|
60
|
+
const blocks = new Map<number, RecordValue>();
|
|
61
|
+
const argumentsByIndex = new Map<number, string>();
|
|
62
|
+
for (const event of events) {
|
|
63
|
+
const index = count(event.index);
|
|
64
|
+
if (event.type === "message_start") {
|
|
65
|
+
response = { ...object(event.message) };
|
|
66
|
+
recognized = true;
|
|
67
|
+
terminalUsage = false;
|
|
68
|
+
} else if (event.type === "content_block_start" && index !== undefined)
|
|
69
|
+
blocks.set(index, { ...object(event.content_block) });
|
|
70
|
+
else if (event.type === "content_block_delta" && index !== undefined) {
|
|
71
|
+
const block = blocks.get(index);
|
|
72
|
+
const delta = object(event.delta);
|
|
73
|
+
if (!block) {
|
|
74
|
+
invalid = true;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (
|
|
78
|
+
!["text_delta", "thinking_delta", "signature_delta", "input_json_delta"].includes(
|
|
79
|
+
String(delta.type),
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
unsupported = true;
|
|
83
|
+
for (const key of ["text", "thinking", "signature"]) append(block, key, delta[key]);
|
|
84
|
+
if (typeof delta.partial_json === "string")
|
|
85
|
+
argumentsByIndex.set(index, (argumentsByIndex.get(index) ?? "") + delta.partial_json);
|
|
86
|
+
} else if (event.type === "message_delta") {
|
|
87
|
+
Object.assign(response, object(event.delta));
|
|
88
|
+
// Message start and final delta are snapshots of different fields, not increments.
|
|
89
|
+
response.usage = { ...object(response.usage), ...object(event.usage) };
|
|
90
|
+
terminalUsage =
|
|
91
|
+
typeof object(event.delta).stop_reason === "string" &&
|
|
92
|
+
count(object(event.usage).output_tokens) !== undefined;
|
|
93
|
+
} else if (event.type === "message_stop") terminal = true;
|
|
94
|
+
else if (event.type === "error") {
|
|
95
|
+
response.error = event.error;
|
|
96
|
+
recognized = true;
|
|
97
|
+
terminal = true;
|
|
98
|
+
terminalUsage = false;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const [index, argumentsText] of argumentsByIndex) {
|
|
102
|
+
const block = blocks.get(index);
|
|
103
|
+
if (block) {
|
|
104
|
+
try {
|
|
105
|
+
block.input = JSON.parse(argumentsText);
|
|
106
|
+
} catch {
|
|
107
|
+
block.partial_json = argumentsText;
|
|
108
|
+
invalid ||= terminal;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
response.content = [...blocks.entries()].toSorted(([a], [b]) => a - b).map(([, value]) => value);
|
|
113
|
+
|
|
114
|
+
return { response, invalid, recognized, unsupported, terminal, terminalUsage };
|
|
115
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { RecordValue } from "./types.js";
|
|
2
|
+
import { append, count, list, object } from "./values.js";
|
|
3
|
+
export function assembleChatStream(events: RecordValue[], done: boolean, stockCompleted = false) {
|
|
4
|
+
const response: RecordValue = {};
|
|
5
|
+
let invalid = false,
|
|
6
|
+
recognized = false,
|
|
7
|
+
unsupported = false,
|
|
8
|
+
terminal = false,
|
|
9
|
+
terminalUsage = false;
|
|
10
|
+
const choices = new Map<number, RecordValue>();
|
|
11
|
+
const tools = new Map<number, Map<number, RecordValue>>();
|
|
12
|
+
for (const event of events) {
|
|
13
|
+
if (Array.isArray(event.choices) || event.error) recognized = true;
|
|
14
|
+
for (const key of ["id", "model", "object"])
|
|
15
|
+
if (event[key] !== undefined) response[key] = event[key];
|
|
16
|
+
if (event.usage !== undefined && event.usage !== null) response.usage = event.usage;
|
|
17
|
+
// Final usage is emitted on an empty choices chunk or with finished choices.
|
|
18
|
+
// A later [DONE] marker cannot turn an earlier running count into final usage.
|
|
19
|
+
if (Array.isArray(event.choices))
|
|
20
|
+
terminalUsage =
|
|
21
|
+
event.usage !== undefined &&
|
|
22
|
+
event.usage !== null &&
|
|
23
|
+
event.choices.every((choice) => typeof object(choice).finish_reason === "string");
|
|
24
|
+
if (event.error) {
|
|
25
|
+
response.error = event.error;
|
|
26
|
+
terminalUsage = false;
|
|
27
|
+
}
|
|
28
|
+
for (const raw of list(event.choices)) {
|
|
29
|
+
const choice = object(raw);
|
|
30
|
+
const index = count(choice.index);
|
|
31
|
+
if (index === undefined) {
|
|
32
|
+
invalid = true;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const saved = choices.get(index) ?? { index, message: { role: "assistant" } };
|
|
36
|
+
const message = object(saved.message);
|
|
37
|
+
const delta = object(choice.delta);
|
|
38
|
+
if (
|
|
39
|
+
Object.keys(delta).some(
|
|
40
|
+
(key) =>
|
|
41
|
+
![
|
|
42
|
+
"role",
|
|
43
|
+
"content",
|
|
44
|
+
"reasoning_content",
|
|
45
|
+
"reasoning",
|
|
46
|
+
"refusal",
|
|
47
|
+
"tool_calls",
|
|
48
|
+
"reasoning_details",
|
|
49
|
+
].includes(key),
|
|
50
|
+
)
|
|
51
|
+
)
|
|
52
|
+
unsupported = true;
|
|
53
|
+
for (const key of ["content", "reasoning_content", "reasoning", "refusal"])
|
|
54
|
+
append(message, key, delta[key]);
|
|
55
|
+
if (delta.reasoning_details !== undefined)
|
|
56
|
+
message.reasoning_details = [
|
|
57
|
+
...list(message.reasoning_details),
|
|
58
|
+
...list(delta.reasoning_details),
|
|
59
|
+
];
|
|
60
|
+
const byIndex = tools.get(index) ?? new Map<number, RecordValue>();
|
|
61
|
+
for (const rawTool of list(delta.tool_calls)) {
|
|
62
|
+
const tool = object(rawTool);
|
|
63
|
+
const toolIndex = count(tool.index);
|
|
64
|
+
if (toolIndex === undefined) {
|
|
65
|
+
invalid = true;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const savedTool = byIndex.get(toolIndex) ?? { type: "function", function: {} };
|
|
69
|
+
if (tool.id !== undefined) savedTool.id = tool.id;
|
|
70
|
+
if (tool.type !== undefined) savedTool.type = tool.type;
|
|
71
|
+
for (const key of ["name", "arguments"])
|
|
72
|
+
append(object(savedTool.function), key, object(tool.function)[key]);
|
|
73
|
+
byIndex.set(toolIndex, savedTool);
|
|
74
|
+
}
|
|
75
|
+
tools.set(index, byIndex);
|
|
76
|
+
if (byIndex.size)
|
|
77
|
+
message.tool_calls = [...byIndex.entries()]
|
|
78
|
+
.toSorted(([a], [b]) => a - b)
|
|
79
|
+
.map(([, value]) => value);
|
|
80
|
+
if (choice.finish_reason !== undefined && choice.finish_reason !== null)
|
|
81
|
+
saved.finish_reason = choice.finish_reason;
|
|
82
|
+
choices.set(index, saved);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
response.choices = [...choices.entries()].toSorted(([a], [b]) => a - b).map(([, value]) => value);
|
|
86
|
+
terminal =
|
|
87
|
+
done ||
|
|
88
|
+
response.error !== undefined ||
|
|
89
|
+
(stockCompleted &&
|
|
90
|
+
choices.size > 0 &&
|
|
91
|
+
[...choices.values()].every((choice) => typeof choice.finish_reason === "string"));
|
|
92
|
+
|
|
93
|
+
return { response, invalid, recognized, unsupported, terminal, terminalUsage };
|
|
94
|
+
}
|