@plurnk/plurnk-providers 1.5.0 → 1.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/.env.defaults +36 -22
- package/SPEC.md +133 -59
- package/dist/AiSdkProvider.d.ts +19 -26
- package/dist/AiSdkProvider.d.ts.map +1 -1
- package/dist/AiSdkProvider.js +318 -106
- package/dist/AiSdkProvider.js.map +1 -1
- package/dist/Mock.d.ts +4 -9
- package/dist/Mock.d.ts.map +1 -1
- package/dist/Mock.js +36 -9
- package/dist/Mock.js.map +1 -1
- package/dist/Pool.d.ts +2 -21
- package/dist/Pool.d.ts.map +1 -1
- package/dist/Pool.js +19 -14
- package/dist/Pool.js.map +1 -1
- package/dist/accounting.d.ts +5 -2
- package/dist/accounting.d.ts.map +1 -1
- package/dist/accounting.js +100 -16
- package/dist/accounting.js.map +1 -1
- package/dist/aiSdkTransport.d.ts +9 -2
- package/dist/aiSdkTransport.d.ts.map +1 -1
- package/dist/aiSdkTransport.js +160 -62
- package/dist/aiSdkTransport.js.map +1 -1
- package/dist/catalogProvider.d.ts +7 -3
- package/dist/catalogProvider.d.ts.map +1 -1
- package/dist/catalogProvider.js +30 -24
- package/dist/catalogProvider.js.map +1 -1
- package/dist/compatibleProvider.d.ts.map +1 -1
- package/dist/compatibleProvider.js +18 -7
- package/dist/compatibleProvider.js.map +1 -1
- package/dist/cost.d.ts +10 -10
- package/dist/cost.d.ts.map +1 -1
- package/dist/cost.js +90 -42
- package/dist/cost.js.map +1 -1
- package/dist/env.d.ts +5 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +30 -10
- package/dist/env.js.map +1 -1
- package/dist/errors.d.ts +14 -2
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +58 -2
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/ollama.js +3 -3
- package/dist/ollama.js.map +1 -1
- package/dist/sdkModels.d.ts +6 -2
- package/dist/sdkModels.d.ts.map +1 -1
- package/dist/sdkModels.js +38 -5
- package/dist/sdkModels.js.map +1 -1
- package/dist/types.d.ts +33 -31
- package/dist/types.d.ts.map +1 -1
- package/dist/usage.d.ts +21 -5
- package/dist/usage.d.ts.map +1 -1
- package/dist/usage.js +164 -83
- package/dist/usage.js.map +1 -1
- package/package.json +7 -6
- package/src/AiSdkProvider.test.ts +788 -191
- package/src/AiSdkProvider.ts +381 -124
- package/src/Mock.test.ts +37 -12
- package/src/Mock.ts +45 -14
- package/src/Pool.test.ts +19 -6
- package/src/Pool.ts +20 -16
- package/src/ProviderRegistry.test.ts +16 -11
- package/src/accounting.test.ts +58 -22
- package/src/accounting.ts +120 -18
- package/src/aiSdkTransport.test.ts +42 -49
- package/src/aiSdkTransport.ts +174 -62
- package/src/boundaries.test.ts +1 -0
- package/src/catalogProvider.test.ts +258 -22
- package/src/catalogProvider.ts +42 -27
- package/src/compatibleProvider.test.ts +6 -3
- package/src/compatibleProvider.ts +20 -7
- package/src/cost.test.ts +55 -36
- package/src/cost.ts +111 -50
- package/src/defaults.test.ts +13 -3
- package/src/env.test.ts +54 -5
- package/src/env.ts +43 -18
- package/src/errors.test.ts +47 -2
- package/src/errors.ts +67 -3
- package/src/index.ts +21 -5
- package/src/ollama.test.ts +4 -1
- package/src/ollama.ts +3 -3
- package/src/sdkModels.test.ts +76 -4
- package/src/sdkModels.ts +45 -7
- package/src/types.ts +77 -38
- package/src/usage.test.ts +112 -116
- package/src/usage.ts +209 -93
package/src/accounting.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type {
|
|
2
|
-
|
|
2
|
+
ChargedCost,
|
|
3
|
+
ProviderAccounting,
|
|
4
|
+
ProviderCost,
|
|
5
|
+
ProviderCostNormalizer,
|
|
6
|
+
ProviderRequestAccounting,
|
|
7
|
+
ProviderUsage,
|
|
3
8
|
} from "./types.ts";
|
|
9
|
+
import {
|
|
10
|
+
sumProviderCostsUsd,
|
|
11
|
+
validateChargedCost,
|
|
12
|
+
validateProviderCost,
|
|
13
|
+
} from "./cost.ts";
|
|
14
|
+
import { validateProviderUsage } from "./usage.ts";
|
|
4
15
|
|
|
5
16
|
const recordOf = (value: unknown): Record<string, unknown> | null =>
|
|
6
17
|
typeof value === "object" && value !== null && !Array.isArray(value)
|
|
@@ -33,7 +44,7 @@ const usdFromTicks = (ticks: number): string => {
|
|
|
33
44
|
return fraction === "" ? integer : `${integer}.${fraction}`;
|
|
34
45
|
};
|
|
35
46
|
|
|
36
|
-
const
|
|
47
|
+
const xaiCost: ProviderCostNormalizer = ({ usage }) => {
|
|
37
48
|
const wireUsage = recordOf(usage);
|
|
38
49
|
if (wireUsage === null || !("cost_in_usd_ticks" in wireUsage)) return undefined;
|
|
39
50
|
const ticks = wireUsage.cost_in_usd_ticks;
|
|
@@ -41,48 +52,139 @@ const xaiCharge: AuthoritativeChargeNormalizer = ({ usage }) => {
|
|
|
41
52
|
throw new TypeError("xAI usage.cost_in_usd_ticks must be numeric");
|
|
42
53
|
}
|
|
43
54
|
return {
|
|
44
|
-
kind: "
|
|
55
|
+
kind: "charged",
|
|
45
56
|
amount: { amount: String(ticks), currency: "USDTICK" },
|
|
46
57
|
usdEquivalent: usdFromTicks(ticks),
|
|
47
58
|
source: "xAI response usage.cost_in_usd_ticks",
|
|
48
59
|
};
|
|
49
60
|
};
|
|
50
61
|
|
|
51
|
-
const
|
|
62
|
+
const openRouterCost: ProviderCostNormalizer = ({ providerMetadata }) => {
|
|
52
63
|
const usage = recordOf(recordOf(recordOf(providerMetadata)?.openrouter)?.usage);
|
|
53
64
|
if (usage === null || !("cost" in usage)) return undefined;
|
|
54
65
|
const cost = usage.cost;
|
|
55
66
|
if (typeof cost !== "number") throw new TypeError("OpenRouter usage.cost must be numeric");
|
|
56
|
-
const amount = decimalFromNumber(cost, "OpenRouter usage.cost");
|
|
57
67
|
return {
|
|
58
|
-
kind: "
|
|
59
|
-
amount: { amount, currency: "USD" },
|
|
60
|
-
usdEquivalent: amount,
|
|
68
|
+
kind: "charged",
|
|
69
|
+
amount: { amount: decimalFromNumber(cost, "OpenRouter usage.cost"), currency: "USD" },
|
|
61
70
|
source: "OpenRouter response usage.cost",
|
|
62
71
|
};
|
|
63
72
|
};
|
|
64
73
|
|
|
65
|
-
const
|
|
74
|
+
const deepInfraCost: ProviderCostNormalizer = ({ usage }) => {
|
|
66
75
|
const wireUsage = recordOf(usage);
|
|
67
76
|
if (wireUsage === null || !("estimated_cost" in wireUsage)) return undefined;
|
|
68
77
|
const cost = wireUsage.estimated_cost;
|
|
69
78
|
if (typeof cost !== "number") throw new TypeError("DeepInfra usage.estimated_cost must be numeric");
|
|
70
|
-
const amount = decimalFromNumber(cost, "DeepInfra usage.estimated_cost");
|
|
71
79
|
return {
|
|
72
|
-
kind: "
|
|
73
|
-
amount: { amount, currency: "USD" },
|
|
74
|
-
usdEquivalent: amount,
|
|
80
|
+
kind: "estimated",
|
|
81
|
+
amount: { amount: decimalFromNumber(cost, "DeepInfra usage.estimated_cost"), currency: "USD" },
|
|
75
82
|
source: "DeepInfra response usage.estimated_cost",
|
|
76
83
|
};
|
|
77
84
|
};
|
|
78
85
|
|
|
79
|
-
|
|
86
|
+
// The first-party endpoint owns the direct charged-cost wire field.
|
|
87
|
+
export const plurnkCostNormalizer: ProviderCostNormalizer = ({ charge }) => {
|
|
88
|
+
if (charge === undefined) return undefined;
|
|
89
|
+
return validateChargedCost(charge) as ChargedCost;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const providerCostNormalizer = (
|
|
80
93
|
sdkPackage: string,
|
|
81
|
-
):
|
|
94
|
+
): ProviderCostNormalizer | undefined => {
|
|
82
95
|
switch (sdkPackage) {
|
|
83
|
-
case "@ai-sdk/xai": return
|
|
84
|
-
case "@ai-sdk/deepinfra": return
|
|
85
|
-
case "@openrouter/ai-sdk-provider": return
|
|
96
|
+
case "@ai-sdk/xai": return xaiCost;
|
|
97
|
+
case "@ai-sdk/deepinfra": return deepInfraCost;
|
|
98
|
+
case "@openrouter/ai-sdk-provider": return openRouterCost;
|
|
86
99
|
default: return undefined;
|
|
87
100
|
}
|
|
88
101
|
};
|
|
102
|
+
|
|
103
|
+
export const validateProviderRequestAccounting = (
|
|
104
|
+
value: unknown,
|
|
105
|
+
): ProviderRequestAccounting => {
|
|
106
|
+
const request = recordOf(value);
|
|
107
|
+
if (request === null) throw new TypeError("provider request accounting must be an object");
|
|
108
|
+
if (typeof request.provider !== "string" || request.provider.length === 0) {
|
|
109
|
+
throw new TypeError("provider request accounting.provider must be non-empty");
|
|
110
|
+
}
|
|
111
|
+
if (typeof request.model !== "string" || request.model.length === 0) {
|
|
112
|
+
throw new TypeError("provider request accounting.model must be non-empty");
|
|
113
|
+
}
|
|
114
|
+
if (request.outcome !== "response" && request.outcome !== "error") {
|
|
115
|
+
throw new TypeError("provider request accounting.outcome must be response or error");
|
|
116
|
+
}
|
|
117
|
+
if (request.status !== undefined
|
|
118
|
+
&& (!Number.isInteger(request.status) || (request.status as number) < 100 || (request.status as number) > 599)) {
|
|
119
|
+
throw new TypeError("provider request accounting.status must be an HTTP status");
|
|
120
|
+
}
|
|
121
|
+
if (request.usage !== undefined) validateProviderUsage(request.usage as ProviderUsage);
|
|
122
|
+
validateProviderCost(request.cost);
|
|
123
|
+
return value as ProviderRequestAccounting;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const sumKnown = (
|
|
127
|
+
requests: readonly ProviderRequestAccounting[],
|
|
128
|
+
read: (usage: ProviderUsage) => number | undefined,
|
|
129
|
+
): number | undefined => {
|
|
130
|
+
const values = requests.map((request) => request.usage === undefined
|
|
131
|
+
? undefined
|
|
132
|
+
: read(request.usage));
|
|
133
|
+
return values.some((value) => value === undefined)
|
|
134
|
+
? undefined
|
|
135
|
+
: (values as number[]).reduce((sum, value) => sum + value, 0);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const aggregateProviderAccounting = (
|
|
139
|
+
values: readonly ProviderRequestAccounting[],
|
|
140
|
+
): ProviderAccounting => {
|
|
141
|
+
const requests = values.map(validateProviderRequestAccounting);
|
|
142
|
+
if (requests.length === 0) {
|
|
143
|
+
return {
|
|
144
|
+
requests: [],
|
|
145
|
+
usage: {
|
|
146
|
+
inputTokens: 0,
|
|
147
|
+
outputTokens: 0,
|
|
148
|
+
totalTokens: 0,
|
|
149
|
+
inputTokenDetails: {
|
|
150
|
+
noCacheTokens: 0,
|
|
151
|
+
cacheReadTokens: 0,
|
|
152
|
+
cacheWriteTokens: 0,
|
|
153
|
+
},
|
|
154
|
+
outputTokenDetails: { textTokens: 0, reasoningTokens: 0 },
|
|
155
|
+
},
|
|
156
|
+
costUsd: "0",
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const inputTokens = sumKnown(requests, (usage) => usage.inputTokens);
|
|
161
|
+
const outputTokens = sumKnown(requests, (usage) => usage.outputTokens);
|
|
162
|
+
const totalTokens = sumKnown(requests, (usage) => usage.totalTokens);
|
|
163
|
+
const noCacheTokens = sumKnown(requests, (usage) => usage.inputTokenDetails?.noCacheTokens);
|
|
164
|
+
const cacheReadTokens = sumKnown(requests, (usage) => usage.inputTokenDetails?.cacheReadTokens);
|
|
165
|
+
const cacheWriteTokens = sumKnown(requests, (usage) => usage.inputTokenDetails?.cacheWriteTokens);
|
|
166
|
+
const textTokens = sumKnown(requests, (usage) => usage.outputTokenDetails?.textTokens);
|
|
167
|
+
const reasoningTokens = sumKnown(requests, (usage) => usage.outputTokenDetails?.reasoningTokens);
|
|
168
|
+
const inputTokenDetails = noCacheTokens === undefined
|
|
169
|
+
&& cacheReadTokens === undefined && cacheWriteTokens === undefined
|
|
170
|
+
? undefined
|
|
171
|
+
: { noCacheTokens, cacheReadTokens, cacheWriteTokens };
|
|
172
|
+
const outputTokenDetails = textTokens === undefined && reasoningTokens === undefined
|
|
173
|
+
? undefined
|
|
174
|
+
: { textTokens, reasoningTokens };
|
|
175
|
+
const usage = inputTokens === undefined && outputTokens === undefined && totalTokens === undefined
|
|
176
|
+
&& inputTokenDetails === undefined && outputTokenDetails === undefined
|
|
177
|
+
? null
|
|
178
|
+
: validateProviderUsage({
|
|
179
|
+
...(inputTokens === undefined ? {} : { inputTokens }),
|
|
180
|
+
...(outputTokens === undefined ? {} : { outputTokens }),
|
|
181
|
+
...(totalTokens === undefined ? {} : { totalTokens }),
|
|
182
|
+
...(inputTokenDetails === undefined ? {} : { inputTokenDetails }),
|
|
183
|
+
...(outputTokenDetails === undefined ? {} : { outputTokenDetails }),
|
|
184
|
+
});
|
|
185
|
+
return {
|
|
186
|
+
requests: [...requests],
|
|
187
|
+
usage,
|
|
188
|
+
costUsd: sumProviderCostsUsd(requests.map(({ cost }) => cost)),
|
|
189
|
+
};
|
|
190
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
+
import { APICallError } from "ai";
|
|
3
4
|
import { executeOpenAICompatible } from "./aiSdkTransport.ts";
|
|
4
5
|
|
|
5
6
|
const request = {
|
|
@@ -9,29 +10,33 @@ const request = {
|
|
|
9
10
|
body: {},
|
|
10
11
|
messages: [{ role: "user" as const, content: "question" }],
|
|
11
12
|
fetchTimeoutMs: 1_000,
|
|
12
|
-
retryAttempts: 2,
|
|
13
13
|
streaming: false,
|
|
14
14
|
captureRawBody: false,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
-
test("
|
|
17
|
+
test("the transport performs exactly one physical request", async () => {
|
|
18
18
|
let calls = 0;
|
|
19
|
-
await assert.rejects(
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
19
|
+
await assert.rejects(
|
|
20
|
+
executeOpenAICompatible({
|
|
21
|
+
...request,
|
|
22
|
+
fetch: async () => {
|
|
23
|
+
calls += 1;
|
|
24
|
+
return new Response(
|
|
25
|
+
JSON.stringify({ error: { message: "upstream attempts exhausted" } }),
|
|
26
|
+
{
|
|
27
|
+
status: 503,
|
|
28
|
+
headers: {
|
|
29
|
+
"content-type": "application/json",
|
|
30
|
+
"x-should-retry": "false",
|
|
31
|
+
},
|
|
30
32
|
},
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
},
|
|
34
|
-
|
|
33
|
+
);
|
|
34
|
+
},
|
|
35
|
+
}),
|
|
36
|
+
(error) => APICallError.isInstance(error)
|
|
37
|
+
&& error.statusCode === 503
|
|
38
|
+
&& error.isRetryable === false,
|
|
39
|
+
);
|
|
35
40
|
assert.equal(calls, 1);
|
|
36
41
|
});
|
|
37
42
|
|
|
@@ -64,7 +69,6 @@ test("the adapter preserves PLURNK request extensions and response evidence", as
|
|
|
64
69
|
};
|
|
65
70
|
const result = await executeOpenAICompatible({
|
|
66
71
|
...request,
|
|
67
|
-
retryAttempts: 0,
|
|
68
72
|
captureRawBody: true,
|
|
69
73
|
body: {
|
|
70
74
|
grammar: "root ::= \"answer\"",
|
|
@@ -85,11 +89,10 @@ test("the adapter preserves PLURNK request extensions and response evidence", as
|
|
|
85
89
|
assert.equal(result.reasoning, "because");
|
|
86
90
|
assert.equal(result.finishReason, "stop");
|
|
87
91
|
assert.deepEqual(result.usage, {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
total: 8,
|
|
92
|
+
inputTokens: 3,
|
|
93
|
+
outputTokens: 5,
|
|
94
|
+
totalTokens: 8,
|
|
95
|
+
outputTokenDetails: { textTokens: 3, reasoningTokens: 2 },
|
|
93
96
|
});
|
|
94
97
|
assert.equal(result.logprobs[0]?.token, "answer");
|
|
95
98
|
assert.deepEqual(result.metadata.balance, { amount: 1.25, currency: "USD" });
|
|
@@ -116,7 +119,6 @@ test("the adapter maps leading system messages to AI SDK instructions", async ()
|
|
|
116
119
|
{ role: "user", content: "hello" },
|
|
117
120
|
],
|
|
118
121
|
fetchTimeoutMs: 1000,
|
|
119
|
-
retryAttempts: 0,
|
|
120
122
|
streaming: false,
|
|
121
123
|
captureRawBody: false,
|
|
122
124
|
fetch,
|
|
@@ -130,7 +132,6 @@ test("the adapter maps leading system messages to AI SDK instructions", async ()
|
|
|
130
132
|
test("the adapter preserves nonstandard reasoning accounting after SDK parsing", async (t) => {
|
|
131
133
|
const execute = (responseBody: object) => executeOpenAICompatible({
|
|
132
134
|
...request,
|
|
133
|
-
retryAttempts: 0,
|
|
134
135
|
fetch: async () => new Response(JSON.stringify(responseBody), {
|
|
135
136
|
headers: { "content-type": "application/json" },
|
|
136
137
|
}),
|
|
@@ -153,25 +154,22 @@ test("the adapter preserves nonstandard reasoning accounting after SDK parsing",
|
|
|
153
154
|
{ prompt_tokens: 2, completion_tokens: 3, total_tokens: 9 },
|
|
154
155
|
));
|
|
155
156
|
assert.deepEqual(result.usage, {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
total: 9,
|
|
157
|
+
inputTokens: 2,
|
|
158
|
+
outputTokens: 7,
|
|
159
|
+
totalTokens: 9,
|
|
160
|
+
outputTokenDetails: { textTokens: 3, reasoningTokens: 4 },
|
|
161
161
|
});
|
|
162
162
|
});
|
|
163
163
|
|
|
164
|
-
await t.test("Fireworks-style
|
|
164
|
+
await t.test("Fireworks-style channels do not invent token attribution", async () => {
|
|
165
165
|
const result = await execute(response(
|
|
166
166
|
{ content: "aa", reasoning_content: "bbbbbb" },
|
|
167
167
|
{ prompt_tokens: 2, completion_tokens: 10, total_tokens: 12 },
|
|
168
168
|
));
|
|
169
169
|
assert.deepEqual(result.usage, {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
cached: 0,
|
|
174
|
-
total: 12,
|
|
170
|
+
inputTokens: 2,
|
|
171
|
+
outputTokens: 10,
|
|
172
|
+
totalTokens: 12,
|
|
175
173
|
});
|
|
176
174
|
});
|
|
177
175
|
|
|
@@ -195,7 +193,6 @@ test("the adapter preserves nonstandard reasoning accounting after SDK parsing",
|
|
|
195
193
|
];
|
|
196
194
|
const result = await executeOpenAICompatible({
|
|
197
195
|
...request,
|
|
198
|
-
retryAttempts: 0,
|
|
199
196
|
streaming: true,
|
|
200
197
|
fetch: async () => new Response(
|
|
201
198
|
`${chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("")}data: [DONE]\n\n`,
|
|
@@ -203,15 +200,14 @@ test("the adapter preserves nonstandard reasoning accounting after SDK parsing",
|
|
|
203
200
|
),
|
|
204
201
|
});
|
|
205
202
|
assert.deepEqual(result.usage, {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
total: 9,
|
|
203
|
+
inputTokens: 2,
|
|
204
|
+
outputTokens: 7,
|
|
205
|
+
totalTokens: 9,
|
|
206
|
+
outputTokenDetails: { textTokens: 3, reasoningTokens: 4 },
|
|
211
207
|
});
|
|
212
208
|
});
|
|
213
209
|
|
|
214
|
-
await t.test("streamed Fireworks-style channels
|
|
210
|
+
await t.test("streamed Fireworks-style channels do not invent an output split", async () => {
|
|
215
211
|
const chunks = [
|
|
216
212
|
{
|
|
217
213
|
id: "response-1",
|
|
@@ -235,7 +231,6 @@ test("the adapter preserves nonstandard reasoning accounting after SDK parsing",
|
|
|
235
231
|
];
|
|
236
232
|
const result = await executeOpenAICompatible({
|
|
237
233
|
...request,
|
|
238
|
-
retryAttempts: 0,
|
|
239
234
|
streaming: true,
|
|
240
235
|
fetch: async () => new Response(
|
|
241
236
|
`${chunks.map((chunk) => `data: ${JSON.stringify(chunk)}\n\n`).join("")}data: [DONE]\n\n`,
|
|
@@ -243,11 +238,9 @@ test("the adapter preserves nonstandard reasoning accounting after SDK parsing",
|
|
|
243
238
|
),
|
|
244
239
|
});
|
|
245
240
|
assert.deepEqual(result.usage, {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
cached: 0,
|
|
250
|
-
total: 12,
|
|
241
|
+
inputTokens: 2,
|
|
242
|
+
outputTokens: 10,
|
|
243
|
+
totalTokens: 12,
|
|
251
244
|
});
|
|
252
245
|
});
|
|
253
246
|
});
|