Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commite4f87d5

Browse files
authored
feat: output cached tokens for<RequestLogsRow /> (#20974)
Closes#21217 This pull-request traverses the `token_usages.metadata[...]` fields andmerges them into a single consumable object so that we're able discerninformation about the metadata within the token usages at a glance. Itsnot the be-all end-all implementation of this feature but its a steppingstone in order to render more useful data to the frontend.These are currently mergable because they only contain `number` basedfields. When it encounters something within the object that can't bemerged (minus empty objects `{}`) it will simply return them as anarray.### Preview<img width="2682" height="1360" alt="CleanShot 2025-11-28 at 15 3009@2x"src="https://github.com/user-attachments/assets/e07e6515-4b8e-4169-841c-38fd83c434f9"/>### Logic breakdown<img width="914" height="1016" alt="CleanShot 2025-11-28 at 15 11 13@2x"src="https://github.com/user-attachments/assets/34b78fe1-3b58-4b78-a552-028ea5a88dc4"/>
1 parent5092645 commite4f87d5

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import{tokenUsageMetadataMerge}from"./RequestLogsRow";
2+
3+
describe("tokenUsageMetadataMerge",()=>{
4+
it("returns null when inputs are null or empty",()=>{
5+
constresult=tokenUsageMetadataMerge(null,{},null);
6+
7+
expect(result).toBeNull();
8+
});
9+
10+
it("returns data when there are no shared keys across metadata",()=>{
11+
constmetadataA={input_tokens:5};
12+
constmetadataB={output_tokens:2};
13+
14+
constresult=tokenUsageMetadataMerge(metadataA,metadataB);
15+
16+
expect(result).toEqual([metadataA,metadataB]);
17+
});
18+
19+
it("sums numeric values for common keys and keeps non-common keys",()=>{
20+
constmetadataA={
21+
input_tokens:5,
22+
model:"gpt-4",
23+
};
24+
constmetadataB={
25+
input_tokens:3,
26+
output_tokens:2,
27+
};
28+
29+
constresult=tokenUsageMetadataMerge(metadataA,metadataB);
30+
31+
expect(result).toEqual({
32+
input_tokens:8,
33+
model:"gpt-4",
34+
output_tokens:2,
35+
});
36+
});
37+
38+
it("preserves identical non-numeric values for common keys",()=>{
39+
constmetadataA={
40+
note:"sync",
41+
status:"ok",
42+
};
43+
constmetadataB={
44+
note:"sync",
45+
status:"ok",
46+
};
47+
48+
constresult=tokenUsageMetadataMerge(metadataA,metadataB);
49+
50+
expect(result).toEqual({
51+
note:"sync",
52+
status:"ok",
53+
});
54+
});
55+
56+
it("returns the original metadata array when a conflict cannot be resolved",()=>{
57+
constmetadataA={
58+
input_tokens:1,
59+
label:"a",
60+
};
61+
constmetadataB={
62+
input_tokens:3,
63+
label:"b",
64+
};
65+
66+
constresult=tokenUsageMetadataMerge(metadataA,metadataB);
67+
68+
expect(result).toEqual([metadataA,metadataB]);
69+
});
70+
});

‎site/src/pages/AIBridgePage/RequestLogsPage/RequestLogsRow/RequestLogsRow.tsx‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,87 @@ type RequestLogsRowProps = {
2121
interception:AIBridgeInterception;
2222
};
2323

24+
typeTokenUsageMetadataMerged=
25+
|null
26+
|Record<string,unknown>
27+
|Array<Record<string,unknown>>;
28+
29+
/**
30+
* This function merges multiple objects with the same keys into a single object.
31+
* It's super unconventional, but it's only a temporary workaround until we
32+
* structure our metadata field for rendering in the UI.
33+
*@param objects - The objects to merge.
34+
*@returns The merged object.
35+
*/
36+
exportfunctiontokenUsageMetadataMerge(
37+
...objects:Array<
38+
AIBridgeInterception["token_usages"][number]["metadata"]|null
39+
>
40+
):TokenUsageMetadataMerged{
41+
constvalidObjects=objects.filter((obj)=>obj!==null);
42+
43+
// Filter out empty objects
44+
constnonEmptyObjects=validObjects.filter(
45+
(obj)=>Object.keys(obj).length>0,
46+
);
47+
if(nonEmptyObjects.length===0){
48+
returnnull;
49+
}
50+
51+
constallKeys=newSet(nonEmptyObjects.flatMap((obj)=>Object.keys(obj)));
52+
constcommonKeys=Array.from(allKeys).filter((key)=>
53+
nonEmptyObjects.every((obj)=>keyinobj),
54+
);
55+
if(commonKeys.length===0){
56+
returnnonEmptyObjects;
57+
}
58+
59+
// Check for unresolvable conflicts: values that aren't all numeric or all
60+
// the same.
61+
for(constkeyofallKeys){
62+
constobjectsWithKey=nonEmptyObjects.filter((obj)=>keyinobj);
63+
if(objectsWithKey.length>1){
64+
constvalues=objectsWithKey.map((obj)=>obj[key]);
65+
constallNumeric=values.every((v:unknown)=>typeofv==="number");
66+
constallSame=newSet(values).size===1;
67+
if(!allNumeric&&!allSame){
68+
returnnonEmptyObjects;
69+
}
70+
}
71+
}
72+
73+
// Merge common keys: sum numeric values, preserve identical values, mark
74+
// conflicts as null.
75+
constresult:Record<string,unknown>={};
76+
for(constkeyofcommonKeys){
77+
constvalues=nonEmptyObjects.map((obj)=>obj[key]);
78+
constallNumeric=values.every((v:unknown)=>typeofv==="number");
79+
constallSame=newSet(values).size===1;
80+
81+
if(allNumeric){
82+
result[key]=values.reduce((acc,v)=>acc+(vasnumber),0);
83+
}elseif(allSame){
84+
result[key]=values[0];
85+
}else{
86+
result[key]=null;
87+
}
88+
}
89+
90+
// Add non-common keys from the first object that has them.
91+
for(constobjofnonEmptyObjects){
92+
for(constkeyofObject.keys(obj)){
93+
if(!commonKeys.includes(key)&&!(keyinresult)){
94+
result[key]=obj[key];
95+
}
96+
}
97+
}
98+
99+
// If any conflicts were marked, return original objects.
100+
returnObject.values(result).some((v:unknown)=>v===null)
101+
?nonEmptyObjects
102+
:result;
103+
}
104+
24105
exportconstRequestLogsRow:FC<RequestLogsRowProps>=({ interception})=>{
25106
const[isOpen,setIsOpen]=useState(false);
26107

@@ -34,6 +115,11 @@ export const RequestLogsRow: FC<RequestLogsRowProps> = ({ interception }) => {
34115
(acc,tokenUsage)=>acc+tokenUsage.output_tokens,
35116
0,
36117
);
118+
119+
consttokenUsagesMetadata=tokenUsageMetadataMerge(
120+
...interception.token_usages.map((tokenUsage)=>tokenUsage.metadata),
121+
);
122+
37123
consttoolCalls=interception.tool_usages.length;
38124
constduration=
39125
interception.ended_at&&
@@ -208,6 +294,15 @@ export const RequestLogsRow: FC<RequestLogsRowProps> = ({ interception }) => {
208294
</div>
209295
</div>
210296
)}
297+
298+
{tokenUsagesMetadata!==null&&(
299+
<divclassName="flex flex-col gap-2">
300+
<div>Token Usage Metadata</div>
301+
<divclassName="bg-surface-secondary rounded-md p-4">
302+
<pre>{JSON.stringify(tokenUsagesMetadata,null,2)}</pre>
303+
</div>
304+
</div>
305+
)}
211306
</div>
212307
</TableCell>
213308
</TableRow>

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp