Skip to content

Commit 75b7e29

Browse files
authored
feat: add getResponse() method for accessing full response with usage data (#92)
2 parents 9073996 + a77b89e commit 75b7e29

File tree

3 files changed

+277
-0
lines changed

3 files changed

+277
-0
lines changed

src/funcs/callModel.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { convertEnhancedToolsToAPIFormat } from "../lib/tool-executor.js";
1414
*
1515
* - `await response.getMessage()` - Get the completed message (tools auto-executed)
1616
* - `await response.getText()` - Get just the text content (tools auto-executed)
17+
* - `await response.getResponse()` - Get full response with usage data (inputTokens, cachedTokens, etc.)
1718
* - `for await (const delta of response.getTextStream())` - Stream text deltas
1819
* - `for await (const delta of response.getReasoningStream())` - Stream reasoning deltas
1920
* - `for await (const event of response.getToolStream())` - Stream tool events (incl. preliminary results)

src/lib/response-wrapper.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,21 @@ export class ResponseWrapper {
325325
return this.textPromise;
326326
}
327327

328+
/**
329+
* Get the complete response object including usage information.
330+
* This will consume the stream until completion and execute any tools.
331+
* Returns the full OpenResponsesNonStreamingResponse with usage data (inputTokens, outputTokens, cachedTokens, etc.)
332+
*/
333+
async getResponse(): Promise<models.OpenResponsesNonStreamingResponse> {
334+
await this.executeToolsIfNeeded();
335+
336+
if (!this.finalResponse) {
337+
throw new Error("Response not available");
338+
}
339+
340+
return this.finalResponse;
341+
}
342+
328343
/**
329344
* Stream all response events as they arrive.
330345
* Multiple consumers can iterate over this stream concurrently.

tests/e2e/callModel.test.ts

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,6 +980,182 @@ describe("callModel E2E Tests", () => {
980980
});
981981
});
982982

983+
describe("response.getResponse - Full response with usage", () => {
984+
it("should return full response with correct shape", async () => {
985+
const response = client.callModel({
986+
model: "meta-llama/llama-3.2-1b-instruct",
987+
input: [
988+
{
989+
role: "user",
990+
content: "Say 'hello'.",
991+
},
992+
],
993+
});
994+
995+
const fullResponse = await response.getResponse();
996+
997+
// Verify top-level response shape
998+
expect(fullResponse).toBeDefined();
999+
expect(typeof fullResponse.id).toBe("string");
1000+
expect(fullResponse.id.length).toBeGreaterThan(0);
1001+
expect(fullResponse.object).toBe("response");
1002+
expect(typeof fullResponse.createdAt).toBe("number");
1003+
expect(typeof fullResponse.model).toBe("string");
1004+
expect(Array.isArray(fullResponse.output)).toBe(true);
1005+
expect(fullResponse.output.length).toBeGreaterThan(0);
1006+
1007+
// Verify output items have correct shape
1008+
for (const item of fullResponse.output) {
1009+
expect(item).toHaveProperty("type");
1010+
expect(typeof (item as any).type).toBe("string");
1011+
}
1012+
1013+
// Verify temperature and topP are present (can be null)
1014+
expect("temperature" in fullResponse).toBe(true);
1015+
expect("topP" in fullResponse).toBe(true);
1016+
1017+
// Verify metadata shape
1018+
expect("metadata" in fullResponse).toBe(true);
1019+
1020+
// Verify tools array exists
1021+
expect(Array.isArray(fullResponse.tools)).toBe(true);
1022+
1023+
// Verify toolChoice exists
1024+
expect("toolChoice" in fullResponse).toBe(true);
1025+
1026+
// Verify parallelToolCalls is boolean
1027+
expect(typeof fullResponse.parallelToolCalls).toBe("boolean");
1028+
});
1029+
1030+
it("should return usage with correct shape including all token details", async () => {
1031+
const response = client.callModel({
1032+
model: "meta-llama/llama-3.2-1b-instruct",
1033+
input: [
1034+
{
1035+
role: "user",
1036+
content: "Say 'hello'.",
1037+
},
1038+
],
1039+
});
1040+
1041+
const fullResponse = await response.getResponse();
1042+
1043+
// Verify usage exists and has correct shape
1044+
expect(fullResponse.usage).toBeDefined();
1045+
const usage = fullResponse.usage!;
1046+
1047+
// Verify top-level usage fields
1048+
expect(typeof usage.inputTokens).toBe("number");
1049+
expect(usage.inputTokens).toBeGreaterThan(0);
1050+
expect(typeof usage.outputTokens).toBe("number");
1051+
expect(usage.outputTokens).toBeGreaterThan(0);
1052+
expect(typeof usage.totalTokens).toBe("number");
1053+
expect(usage.totalTokens).toBe(usage.inputTokens + usage.outputTokens);
1054+
1055+
// Verify inputTokensDetails shape with cachedTokens
1056+
expect(usage.inputTokensDetails).toBeDefined();
1057+
expect(typeof usage.inputTokensDetails.cachedTokens).toBe("number");
1058+
expect(usage.inputTokensDetails.cachedTokens).toBeGreaterThanOrEqual(0);
1059+
1060+
// Verify outputTokensDetails shape with reasoningTokens
1061+
expect(usage.outputTokensDetails).toBeDefined();
1062+
expect(typeof usage.outputTokensDetails.reasoningTokens).toBe("number");
1063+
expect(usage.outputTokensDetails.reasoningTokens).toBeGreaterThanOrEqual(0);
1064+
1065+
// Verify optional cost fields if present
1066+
if (usage.cost !== undefined && usage.cost !== null) {
1067+
expect(typeof usage.cost).toBe("number");
1068+
}
1069+
1070+
if (usage.isByok !== undefined) {
1071+
expect(typeof usage.isByok).toBe("boolean");
1072+
}
1073+
1074+
if (usage.costDetails !== undefined) {
1075+
expect(typeof usage.costDetails.upstreamInferenceInputCost).toBe("number");
1076+
expect(typeof usage.costDetails.upstreamInferenceOutputCost).toBe("number");
1077+
if (usage.costDetails.upstreamInferenceCost !== undefined && usage.costDetails.upstreamInferenceCost !== null) {
1078+
expect(typeof usage.costDetails.upstreamInferenceCost).toBe("number");
1079+
}
1080+
}
1081+
});
1082+
1083+
it("should return error and incompleteDetails fields with correct shape", async () => {
1084+
const response = client.callModel({
1085+
model: "meta-llama/llama-3.2-1b-instruct",
1086+
input: [
1087+
{
1088+
role: "user",
1089+
content: "Say 'test'.",
1090+
},
1091+
],
1092+
});
1093+
1094+
const fullResponse = await response.getResponse();
1095+
1096+
// error can be null or an object
1097+
expect("error" in fullResponse).toBe(true);
1098+
if (fullResponse.error !== null) {
1099+
expect(typeof fullResponse.error).toBe("object");
1100+
}
1101+
1102+
// incompleteDetails can be null or an object
1103+
expect("incompleteDetails" in fullResponse).toBe(true);
1104+
if (fullResponse.incompleteDetails !== null) {
1105+
expect(typeof fullResponse.incompleteDetails).toBe("object");
1106+
}
1107+
});
1108+
1109+
it("should allow concurrent access with other methods", async () => {
1110+
const response = client.callModel({
1111+
model: "meta-llama/llama-3.2-1b-instruct",
1112+
input: [
1113+
{
1114+
role: "user",
1115+
content: "Say 'test'.",
1116+
},
1117+
],
1118+
});
1119+
1120+
// Get both text and full response concurrently
1121+
const [text, fullResponse] = await Promise.all([
1122+
response.getText(),
1123+
response.getResponse(),
1124+
]);
1125+
1126+
expect(text).toBeDefined();
1127+
expect(typeof text).toBe("string");
1128+
expect(fullResponse).toBeDefined();
1129+
expect(fullResponse.usage).toBeDefined();
1130+
1131+
// Text should match outputText
1132+
if (fullResponse.outputText) {
1133+
expect(text).toBe(fullResponse.outputText);
1134+
}
1135+
});
1136+
1137+
it("should return consistent results on multiple calls", async () => {
1138+
const response = client.callModel({
1139+
model: "meta-llama/llama-3.2-1b-instruct",
1140+
input: [
1141+
{
1142+
role: "user",
1143+
content: "Say 'consistent'.",
1144+
},
1145+
],
1146+
});
1147+
1148+
const firstCall = await response.getResponse();
1149+
const secondCall = await response.getResponse();
1150+
1151+
// Should return the same response object
1152+
expect(firstCall.id).toBe(secondCall.id);
1153+
expect(firstCall.usage?.inputTokens).toBe(secondCall.usage?.inputTokens);
1154+
expect(firstCall.usage?.outputTokens).toBe(secondCall.usage?.outputTokens);
1155+
expect(firstCall.usage?.inputTokensDetails?.cachedTokens).toBe(secondCall.usage?.inputTokensDetails?.cachedTokens);
1156+
});
1157+
});
1158+
9831159
describe("Response parameters", () => {
9841160
it("should respect maxOutputTokens parameter", async () => {
9851161
const response = client.callModel({
@@ -1019,5 +1195,90 @@ describe("callModel E2E Tests", () => {
10191195
expect(text.length).toBeGreaterThan(0);
10201196
// Just verify instructions parameter is accepted, not that model follows it perfectly
10211197
});
1198+
1199+
it("should support provider parameter with correct shape", async () => {
1200+
const response = client.callModel({
1201+
model: "meta-llama/llama-3.2-1b-instruct",
1202+
input: [
1203+
{
1204+
role: "user",
1205+
content: "Say 'provider test'.",
1206+
},
1207+
],
1208+
provider: {
1209+
allowFallbacks: true,
1210+
requireParameters: false,
1211+
},
1212+
});
1213+
1214+
const fullResponse = await response.getResponse();
1215+
1216+
expect(fullResponse).toBeDefined();
1217+
expect(fullResponse.usage).toBeDefined();
1218+
expect(fullResponse.usage?.inputTokens).toBeGreaterThan(0);
1219+
});
1220+
1221+
it("should support provider with order preference", async () => {
1222+
const response = client.callModel({
1223+
model: "meta-llama/llama-3.2-1b-instruct",
1224+
input: [
1225+
{
1226+
role: "user",
1227+
content: "Say 'ordered provider'.",
1228+
},
1229+
],
1230+
provider: {
1231+
order: ["Together", "Fireworks"],
1232+
allowFallbacks: true,
1233+
},
1234+
});
1235+
1236+
const text = await response.getText();
1237+
1238+
expect(text).toBeDefined();
1239+
expect(typeof text).toBe("string");
1240+
expect(text.length).toBeGreaterThan(0);
1241+
});
1242+
1243+
it("should support provider with ignore list", async () => {
1244+
const response = client.callModel({
1245+
model: "meta-llama/llama-3.2-1b-instruct",
1246+
input: [
1247+
{
1248+
role: "user",
1249+
content: "Say 'ignore test'.",
1250+
},
1251+
],
1252+
provider: {
1253+
ignore: ["SomeProvider"],
1254+
allowFallbacks: true,
1255+
},
1256+
});
1257+
1258+
const text = await response.getText();
1259+
1260+
expect(text).toBeDefined();
1261+
expect(typeof text).toBe("string");
1262+
});
1263+
1264+
it("should support provider with quantizations filter", async () => {
1265+
const response = client.callModel({
1266+
model: "meta-llama/llama-3.2-1b-instruct",
1267+
input: [
1268+
{
1269+
role: "user",
1270+
content: "Say 'quantization test'.",
1271+
},
1272+
],
1273+
provider: {
1274+
allowFallbacks: true,
1275+
},
1276+
});
1277+
1278+
const fullResponse = await response.getResponse();
1279+
1280+
expect(fullResponse).toBeDefined();
1281+
expect(fullResponse.model).toBeDefined();
1282+
});
10221283
});
10231284
});

0 commit comments

Comments
 (0)