tinker-agent 1.11.0 → 2.0.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 +17 -1
- package/README.md +7 -23
- package/package.json +1 -1
- package/src/agent/context-meter.ts +14 -85
- package/src/agent/loop.ts +1 -14
- package/src/agent/runtime-session.ts +0 -23
- package/src/cli/config.ts +0 -5
- package/src/cli/model-profiles.ts +0 -98
- package/src/cli/public-config-contract.ts +1 -88
- package/src/cli/runner-dependencies.ts +0 -7
- package/src/cli/tui-memory.ts +0 -3
- package/src/image/image-input-policy.ts +53 -2
- package/src/image/image-probe.ts +8 -2
- package/src/image/provider-image.ts +99 -0
- package/src/model/fake-model-client.ts +69 -69
- package/src/model/model-client.ts +2 -2
- package/src/model/model-request-preflight.ts +0 -1
- package/src/model/openai-chat-model-client.ts +4 -28
- package/src/model/openai-model-utils.ts +69 -31
- package/src/model/openai-responses-model-client.ts +0 -29
- package/src/model/token-estimator.ts +16 -3
- package/src/session/session-store.ts +13 -23
- package/src/model/input-token-estimator.ts +0 -25
- package/src/model/moonshot-input-token-estimator.ts +0 -111
- package/src/model/openai-responses-token-estimator.ts +0 -155
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
IMAGE_INPUT_POLICY,
|
|
9
9
|
IMAGE_INPUT_POLICY_VERSION,
|
|
10
10
|
} from "../image/image-input-policy";
|
|
11
|
-
import type { InputTokenEstimator } from "./input-token-estimator";
|
|
12
11
|
import type { ModelContextBudget } from "./model-context-profile";
|
|
13
12
|
import { ProviderResponseError } from "./model-client";
|
|
14
13
|
import type {
|
|
@@ -22,7 +21,6 @@ import type {
|
|
|
22
21
|
PreparedModelRequest,
|
|
23
22
|
PreparedPromptSegment,
|
|
24
23
|
} from "./model-client";
|
|
25
|
-
import { MoonshotInputTokenEstimator } from "./moonshot-input-token-estimator";
|
|
26
24
|
import {
|
|
27
25
|
deepFreeze,
|
|
28
26
|
imageUserSegment,
|
|
@@ -38,7 +36,6 @@ import {
|
|
|
38
36
|
toOpenAIResponsesTools,
|
|
39
37
|
} from "./openai-responses-mapping";
|
|
40
38
|
import { OpenAIResponsesStreamAccumulator } from "./openai-responses-stream";
|
|
41
|
-
import { responsesPayloadForChatTokenEstimator } from "./openai-responses-token-estimator";
|
|
42
39
|
import type { ReasoningEffortController } from "./reasoning-effort";
|
|
43
40
|
import { sha256, stableJsonStringify } from "./model-request-preflight";
|
|
44
41
|
|
|
@@ -50,7 +47,6 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
50
47
|
adapter: "openai-responses",
|
|
51
48
|
serializationVersion: OPENAI_RESPONSES_SERIALIZATION_VERSION,
|
|
52
49
|
});
|
|
53
|
-
readonly inputTokenEstimator?: InputTokenEstimator;
|
|
54
50
|
readonly inputModalities: readonly ("text" | "image")[];
|
|
55
51
|
readonly reasoningEffort?: ReasoningEffortController;
|
|
56
52
|
private readonly client: OpenAI;
|
|
@@ -65,14 +61,6 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
65
61
|
contextBudget: ModelContextBudget;
|
|
66
62
|
baseURL?: string;
|
|
67
63
|
inputModalities?: readonly ("text" | "image")[];
|
|
68
|
-
tokenEstimator?: {
|
|
69
|
-
kind: "moonshot-estimate-token-count-v1";
|
|
70
|
-
model: string;
|
|
71
|
-
apiBase: string;
|
|
72
|
-
apiKey: string;
|
|
73
|
-
timeoutMs: number;
|
|
74
|
-
maxRetries: 0;
|
|
75
|
-
};
|
|
76
64
|
model: string;
|
|
77
65
|
providerName?: string;
|
|
78
66
|
reasoningEffort?: ReasoningEffortController;
|
|
@@ -85,13 +73,9 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
85
73
|
this.stream = options.stream ?? true;
|
|
86
74
|
this.reasoningEffort = options.reasoningEffort;
|
|
87
75
|
this.inputModalities = Object.freeze([...(options.inputModalities ?? ["text"])]);
|
|
88
|
-
const supportsImages = this.inputModalities.includes("image");
|
|
89
76
|
if (!this.inputModalities.includes("text")) {
|
|
90
77
|
throw new Error('OpenAI Responses input modalities must include "text".');
|
|
91
78
|
}
|
|
92
|
-
if (supportsImages && options.tokenEstimator === undefined) {
|
|
93
|
-
throw new Error("Image-capable OpenAI Responses requires a token estimator.");
|
|
94
|
-
}
|
|
95
79
|
this.client = new OpenAI({
|
|
96
80
|
apiKey: options.apiKey,
|
|
97
81
|
baseURL: options.baseURL,
|
|
@@ -99,16 +83,6 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
99
83
|
maxRetries: 0,
|
|
100
84
|
fetch: options.fetch,
|
|
101
85
|
});
|
|
102
|
-
if (options.tokenEstimator !== undefined) {
|
|
103
|
-
this.inputTokenEstimator = new MoonshotInputTokenEstimator({
|
|
104
|
-
apiKey: options.tokenEstimator.apiKey,
|
|
105
|
-
baseURL: options.tokenEstimator.apiBase,
|
|
106
|
-
model: options.tokenEstimator.model,
|
|
107
|
-
timeoutMs: options.tokenEstimator.timeoutMs,
|
|
108
|
-
fetch: options.fetch,
|
|
109
|
-
payloadMapper: responsesPayloadForChatTokenEstimator,
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
86
|
}
|
|
113
87
|
|
|
114
88
|
prepare(input: ModelRequestInput): PreparedModelRequest {
|
|
@@ -163,9 +137,6 @@ export class OpenAIResponsesModelClient implements ModelClient {
|
|
|
163
137
|
version: IMAGE_INPUT_POLICY_VERSION,
|
|
164
138
|
...IMAGE_INPUT_POLICY,
|
|
165
139
|
},
|
|
166
|
-
...(this.inputTokenEstimator === undefined
|
|
167
|
-
? {}
|
|
168
|
-
: { tokenEstimator: this.inputTokenEstimator.compatibility }),
|
|
169
140
|
}),
|
|
170
141
|
);
|
|
171
142
|
const prepared: PreparedModelRequest = {
|
|
@@ -12,10 +12,15 @@ export type RawContextBreakdown = {
|
|
|
12
12
|
toolTokens: number;
|
|
13
13
|
toolSchemaTokens: number;
|
|
14
14
|
protocolTokens: number;
|
|
15
|
+
textAndProtocolTokens: number;
|
|
16
|
+
imageTokens: number;
|
|
15
17
|
totalTokens: number;
|
|
16
18
|
};
|
|
17
19
|
|
|
18
|
-
type BreakdownKey = Exclude<
|
|
20
|
+
type BreakdownKey = Exclude<
|
|
21
|
+
keyof RawContextBreakdown,
|
|
22
|
+
"totalTokens" | "textAndProtocolTokens" | "imageTokens"
|
|
23
|
+
>;
|
|
19
24
|
|
|
20
25
|
const breakdownKeys: BreakdownKey[] = [
|
|
21
26
|
"kernelTokens",
|
|
@@ -37,13 +42,16 @@ export function estimatePromptSegments(
|
|
|
37
42
|
toolSchemaTokens: 0,
|
|
38
43
|
protocolTokens: segments.length * 8,
|
|
39
44
|
};
|
|
45
|
+
let imageTokens = 0;
|
|
40
46
|
|
|
41
47
|
for (const segment of segments) {
|
|
42
48
|
exact[breakdownKey(segment.kind)] += estimateText(segment.normalizedText);
|
|
43
|
-
|
|
49
|
+
const segmentImageTokens = (segment.media ?? []).reduce(
|
|
44
50
|
(total, media) => total + media.planningTokens,
|
|
45
51
|
0,
|
|
46
52
|
);
|
|
53
|
+
exact[breakdownKey(segment.kind)] += segmentImageTokens;
|
|
54
|
+
imageTokens += segmentImageTokens;
|
|
47
55
|
}
|
|
48
56
|
|
|
49
57
|
const totalTokens = Math.ceil(
|
|
@@ -69,7 +77,12 @@ export function estimatePromptSegments(
|
|
|
69
77
|
remaining -= 1;
|
|
70
78
|
}
|
|
71
79
|
|
|
72
|
-
return {
|
|
80
|
+
return {
|
|
81
|
+
...rounded,
|
|
82
|
+
textAndProtocolTokens: totalTokens - imageTokens,
|
|
83
|
+
imageTokens,
|
|
84
|
+
totalTokens,
|
|
85
|
+
};
|
|
73
86
|
}
|
|
74
87
|
|
|
75
88
|
export class RollingTokenCalibration {
|
|
@@ -32,7 +32,6 @@ import {
|
|
|
32
32
|
MODEL_MESSAGE_PROTOCOL_ADAPTERS,
|
|
33
33
|
type ModelMessageProtocol,
|
|
34
34
|
} from "../model/model-client";
|
|
35
|
-
import type { InputTokenEstimatorCompatibility } from "../model/input-token-estimator";
|
|
36
35
|
import type { ToolDefinition, ToolRawResult } from "../tools/types";
|
|
37
36
|
import { sha256, stableJsonStringify } from "../model/model-request-preflight";
|
|
38
37
|
import {
|
|
@@ -149,7 +148,16 @@ export type SessionImageInputCompatibility = {
|
|
|
149
148
|
readonly policyVersion: string;
|
|
150
149
|
readonly policySha256: string;
|
|
151
150
|
readonly inputModalities: readonly ("text" | "image")[];
|
|
152
|
-
readonly tokenEstimator?:
|
|
151
|
+
readonly tokenEstimator?: LegacyInputTokenEstimatorCompatibility;
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
type LegacyInputTokenEstimatorCompatibility = {
|
|
155
|
+
readonly kind: "moonshot-estimate-token-count-v1";
|
|
156
|
+
readonly coverageVersion: "full-request-v1";
|
|
157
|
+
readonly model: string;
|
|
158
|
+
readonly endpoint: string;
|
|
159
|
+
readonly timeoutMs: number;
|
|
160
|
+
readonly maxRetries: 0;
|
|
153
161
|
};
|
|
154
162
|
|
|
155
163
|
export type CompletedTurnMessageSnapshot =
|
|
@@ -3704,7 +3712,6 @@ export function createSessionCompatibilityContract(input: {
|
|
|
3704
3712
|
contextProfile: ModelContextProfile;
|
|
3705
3713
|
messageProtocol: ModelMessageProtocol;
|
|
3706
3714
|
inputModalities?: readonly ("text" | "image")[];
|
|
3707
|
-
tokenEstimator?: InputTokenEstimatorCompatibility;
|
|
3708
3715
|
}): SessionCompatibilityContract {
|
|
3709
3716
|
if (input.modelName.trim() === "") {
|
|
3710
3717
|
throw new Error("Session compatibility model name must not be empty.");
|
|
@@ -3722,14 +3729,6 @@ export function createSessionCompatibilityContract(input: {
|
|
|
3722
3729
|
throw new Error("Session compatibility message protocol is invalid.");
|
|
3723
3730
|
}
|
|
3724
3731
|
const inputModalities = normalizeInputModalities(input.inputModalities ?? ["text"]);
|
|
3725
|
-
if (inputModalities.includes("image") && input.tokenEstimator === undefined) {
|
|
3726
|
-
throw new Error(
|
|
3727
|
-
"Session compatibility image input requires a token estimator identity.",
|
|
3728
|
-
);
|
|
3729
|
-
}
|
|
3730
|
-
if (input.tokenEstimator !== undefined) {
|
|
3731
|
-
validateTokenEstimatorCompatibility(input.tokenEstimator);
|
|
3732
|
-
}
|
|
3733
3732
|
return Object.freeze({
|
|
3734
3733
|
modelName: input.modelName,
|
|
3735
3734
|
...(input.profileName === undefined ? {} : { profileName: input.profileName }),
|
|
@@ -3745,9 +3744,6 @@ export function createSessionCompatibilityContract(input: {
|
|
|
3745
3744
|
}),
|
|
3746
3745
|
),
|
|
3747
3746
|
inputModalities,
|
|
3748
|
-
...(input.tokenEstimator === undefined
|
|
3749
|
-
? {}
|
|
3750
|
-
: { tokenEstimator: immutableCanonicalClone(input.tokenEstimator) }),
|
|
3751
3747
|
}),
|
|
3752
3748
|
});
|
|
3753
3749
|
}
|
|
@@ -3764,9 +3760,6 @@ function normalizeSessionCompatibilityContract(
|
|
|
3764
3760
|
contextProfile: contract.contextProfile,
|
|
3765
3761
|
messageProtocol: contract.messageProtocol,
|
|
3766
3762
|
inputModalities: contract.imageInput.inputModalities,
|
|
3767
|
-
...(contract.imageInput.tokenEstimator === undefined
|
|
3768
|
-
? {}
|
|
3769
|
-
: { tokenEstimator: contract.imageInput.tokenEstimator }),
|
|
3770
3763
|
});
|
|
3771
3764
|
}
|
|
3772
3765
|
|
|
@@ -3787,7 +3780,7 @@ function normalizeInputModalities(
|
|
|
3787
3780
|
}
|
|
3788
3781
|
|
|
3789
3782
|
function validateTokenEstimatorCompatibility(
|
|
3790
|
-
estimator:
|
|
3783
|
+
estimator: LegacyInputTokenEstimatorCompatibility,
|
|
3791
3784
|
): void {
|
|
3792
3785
|
if (
|
|
3793
3786
|
estimator.kind !== "moonshot-estimate-token-count-v1" ||
|
|
@@ -5594,9 +5587,6 @@ function decodeSessionCompatibilityContract(
|
|
|
5594
5587
|
enumFromSql(value, ["text", "image"] as const, "compatibility input modality"),
|
|
5595
5588
|
),
|
|
5596
5589
|
);
|
|
5597
|
-
if (modalities.includes("image") && tokenEstimator === undefined) {
|
|
5598
|
-
throw new Error("Stored image input compatibility has no token estimator.");
|
|
5599
|
-
}
|
|
5600
5590
|
const policyVersion = stringFromSql(
|
|
5601
5591
|
imageInput.policyVersion,
|
|
5602
5592
|
"compatibility image policyVersion",
|
|
@@ -5625,7 +5615,7 @@ function decodeSessionCompatibilityContract(
|
|
|
5625
5615
|
|
|
5626
5616
|
function decodeTokenEstimatorCompatibility(
|
|
5627
5617
|
value: unknown,
|
|
5628
|
-
):
|
|
5618
|
+
): LegacyInputTokenEstimatorCompatibility {
|
|
5629
5619
|
const record = recordFromSql(value, "session compatibility token estimator");
|
|
5630
5620
|
assertObjectKeys(
|
|
5631
5621
|
record,
|
|
@@ -5633,7 +5623,7 @@ function decodeTokenEstimatorCompatibility(
|
|
|
5633
5623
|
["kind", "coverageVersion", "model", "endpoint", "timeoutMs", "maxRetries"],
|
|
5634
5624
|
"session compatibility token estimator",
|
|
5635
5625
|
);
|
|
5636
|
-
const estimator:
|
|
5626
|
+
const estimator: LegacyInputTokenEstimatorCompatibility = {
|
|
5637
5627
|
kind: enumFromSql(
|
|
5638
5628
|
record.kind,
|
|
5639
5629
|
["moonshot-estimate-token-count-v1"] as const,
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { MaterializedModelRequest } from "./model-client";
|
|
2
|
-
|
|
3
|
-
export type InputTokenEstimate = {
|
|
4
|
-
readonly inputTokens: number;
|
|
5
|
-
readonly source: "provider_estimated";
|
|
6
|
-
readonly coverage: "messages" | "full_request";
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
export type InputTokenEstimatorCompatibility = {
|
|
10
|
-
readonly kind: "moonshot-estimate-token-count-v1";
|
|
11
|
-
readonly coverageVersion: "full-request-v1";
|
|
12
|
-
readonly model: string;
|
|
13
|
-
readonly endpoint: string;
|
|
14
|
-
readonly timeoutMs: number;
|
|
15
|
-
readonly maxRetries: 0;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
export interface InputTokenEstimator {
|
|
19
|
-
readonly kind: string;
|
|
20
|
-
readonly compatibility: InputTokenEstimatorCompatibility;
|
|
21
|
-
estimate(
|
|
22
|
-
request: MaterializedModelRequest,
|
|
23
|
-
options: { signal: AbortSignal },
|
|
24
|
-
): Promise<InputTokenEstimate>;
|
|
25
|
-
}
|
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import type { InputTokenEstimate, InputTokenEstimator } from "./input-token-estimator";
|
|
2
|
-
import type { MaterializedModelRequest } from "./model-client";
|
|
3
|
-
import { IMAGE_INPUT_POLICY } from "../image/image-input-policy";
|
|
4
|
-
|
|
5
|
-
export class MoonshotInputTokenEstimator implements InputTokenEstimator {
|
|
6
|
-
readonly kind = "moonshot-estimate-token-count-v1";
|
|
7
|
-
readonly compatibility: InputTokenEstimator["compatibility"];
|
|
8
|
-
private readonly endpoint: string;
|
|
9
|
-
|
|
10
|
-
constructor(
|
|
11
|
-
private readonly options: {
|
|
12
|
-
apiKey: string;
|
|
13
|
-
baseURL: string;
|
|
14
|
-
model: string;
|
|
15
|
-
timeoutMs: number;
|
|
16
|
-
fetch?: typeof fetch;
|
|
17
|
-
payloadMapper?: (payload: unknown) => unknown;
|
|
18
|
-
},
|
|
19
|
-
) {
|
|
20
|
-
const base = new URL(
|
|
21
|
-
options.baseURL.endsWith("/") ? options.baseURL : `${options.baseURL}/`,
|
|
22
|
-
);
|
|
23
|
-
base.username = "";
|
|
24
|
-
base.password = "";
|
|
25
|
-
base.search = "";
|
|
26
|
-
base.hash = "";
|
|
27
|
-
const endpoint = new URL("tokenizers/estimate-token-count", base);
|
|
28
|
-
this.endpoint = endpoint.toString();
|
|
29
|
-
this.compatibility = Object.freeze({
|
|
30
|
-
kind: "moonshot-estimate-token-count-v1",
|
|
31
|
-
coverageVersion: "full-request-v1",
|
|
32
|
-
model: options.model,
|
|
33
|
-
endpoint: this.endpoint,
|
|
34
|
-
timeoutMs: options.timeoutMs,
|
|
35
|
-
maxRetries: 0,
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
async estimate(
|
|
40
|
-
request: MaterializedModelRequest,
|
|
41
|
-
options: { signal: AbortSignal },
|
|
42
|
-
): Promise<InputTokenEstimate> {
|
|
43
|
-
const chatPayload = requireRecord(
|
|
44
|
-
this.options.payloadMapper?.(request.payload) ?? request.payload,
|
|
45
|
-
"materialized chat payload",
|
|
46
|
-
);
|
|
47
|
-
if (!Array.isArray(chatPayload.messages)) {
|
|
48
|
-
throw new Error("Materialized request has no token estimator messages.");
|
|
49
|
-
}
|
|
50
|
-
const payload = {
|
|
51
|
-
model: this.options.model,
|
|
52
|
-
messages: chatPayload.messages,
|
|
53
|
-
...(Array.isArray(chatPayload.tools) ? { tools: chatPayload.tools } : {}),
|
|
54
|
-
};
|
|
55
|
-
const body = JSON.stringify(payload);
|
|
56
|
-
const bodyBytes = Buffer.byteLength(body, "utf8");
|
|
57
|
-
if (bodyBytes > IMAGE_INPUT_POLICY.maxRequestBodyBytes) {
|
|
58
|
-
throw new Error(
|
|
59
|
-
`Token estimate request is ${bodyBytes} bytes; maximum is ${IMAGE_INPUT_POLICY.maxRequestBodyBytes}.`,
|
|
60
|
-
);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const controller = new AbortController();
|
|
64
|
-
const timeout = setTimeout(
|
|
65
|
-
() => controller.abort(new Error("Token estimate timed out.")),
|
|
66
|
-
this.options.timeoutMs,
|
|
67
|
-
);
|
|
68
|
-
const onAbort = () => controller.abort(options.signal.reason);
|
|
69
|
-
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
70
|
-
try {
|
|
71
|
-
options.signal.throwIfAborted();
|
|
72
|
-
const response = await (this.options.fetch ?? fetch)(this.endpoint, {
|
|
73
|
-
method: "POST",
|
|
74
|
-
headers: {
|
|
75
|
-
authorization: `Bearer ${this.options.apiKey}`,
|
|
76
|
-
"content-type": "application/json",
|
|
77
|
-
},
|
|
78
|
-
body,
|
|
79
|
-
signal: controller.signal,
|
|
80
|
-
});
|
|
81
|
-
if (!response.ok) {
|
|
82
|
-
throw new Error(`Token estimate endpoint returned HTTP ${response.status}.`);
|
|
83
|
-
}
|
|
84
|
-
const decoded: unknown = await response.json();
|
|
85
|
-
const root = requireRecord(decoded, "token estimate response");
|
|
86
|
-
if (root.error !== undefined && root.error !== null) {
|
|
87
|
-
throw new Error("Token estimate endpoint returned an error response.");
|
|
88
|
-
}
|
|
89
|
-
const data = requireRecord(root.data, "token estimate response data");
|
|
90
|
-
const inputTokens = data.total_tokens;
|
|
91
|
-
if (!Number.isSafeInteger(inputTokens) || (inputTokens as number) < 0) {
|
|
92
|
-
throw new Error("Token estimate response total_tokens is invalid.");
|
|
93
|
-
}
|
|
94
|
-
return Object.freeze({
|
|
95
|
-
inputTokens: inputTokens as number,
|
|
96
|
-
source: "provider_estimated",
|
|
97
|
-
coverage: "full_request",
|
|
98
|
-
});
|
|
99
|
-
} finally {
|
|
100
|
-
clearTimeout(timeout);
|
|
101
|
-
options.signal.removeEventListener("abort", onAbort);
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
function requireRecord(value: unknown, name: string): Record<string, unknown> {
|
|
107
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
108
|
-
throw new Error(`${name} must be an object.`);
|
|
109
|
-
}
|
|
110
|
-
return value as Record<string, unknown>;
|
|
111
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
type ChatEstimatorToolCall = {
|
|
2
|
-
id: string;
|
|
3
|
-
type: "function";
|
|
4
|
-
function: {
|
|
5
|
-
name: string;
|
|
6
|
-
arguments: string;
|
|
7
|
-
};
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
type ChatEstimatorMessage = {
|
|
11
|
-
role: "system" | "user" | "assistant" | "tool";
|
|
12
|
-
content: unknown;
|
|
13
|
-
tool_calls?: ChatEstimatorToolCall[];
|
|
14
|
-
tool_call_id?: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export function responsesPayloadForChatTokenEstimator(
|
|
18
|
-
payload: unknown,
|
|
19
|
-
): Record<string, unknown> {
|
|
20
|
-
const root = requireRecord(payload, "Responses token estimator payload");
|
|
21
|
-
if (!Array.isArray(root.input)) {
|
|
22
|
-
throw new Error("Responses token estimator payload input must be an array.");
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
const messages: ChatEstimatorMessage[] = [];
|
|
26
|
-
for (const [index, rawItem] of root.input.entries()) {
|
|
27
|
-
const path = `Responses token estimator input[${index}]`;
|
|
28
|
-
const item = requireRecord(rawItem, path);
|
|
29
|
-
const type = requireString(item.type, `${path}.type`);
|
|
30
|
-
if (type === "message") {
|
|
31
|
-
messages.push(toChatMessage(item, path));
|
|
32
|
-
continue;
|
|
33
|
-
}
|
|
34
|
-
if (type === "function_call") {
|
|
35
|
-
appendFunctionCall(messages, item, path);
|
|
36
|
-
continue;
|
|
37
|
-
}
|
|
38
|
-
if (type === "function_call_output") {
|
|
39
|
-
messages.push({
|
|
40
|
-
role: "tool",
|
|
41
|
-
tool_call_id: requireString(item.call_id, `${path}.call_id`),
|
|
42
|
-
content: requireString(item.output, `${path}.output`),
|
|
43
|
-
});
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
throw new Error(`${path}.type is unsupported: ${JSON.stringify(type)}.`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return {
|
|
50
|
-
messages,
|
|
51
|
-
...(root.tools === undefined ? {} : { tools: toChatTools(root.tools) }),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function toChatMessage(
|
|
56
|
-
item: Record<string, unknown>,
|
|
57
|
-
path: string,
|
|
58
|
-
): ChatEstimatorMessage {
|
|
59
|
-
const role = requireString(item.role, `${path}.role`);
|
|
60
|
-
if (role !== "system" && role !== "user" && role !== "assistant") {
|
|
61
|
-
throw new Error(`${path}.role is unsupported: ${JSON.stringify(role)}.`);
|
|
62
|
-
}
|
|
63
|
-
return {
|
|
64
|
-
role,
|
|
65
|
-
content: toChatContent(item.content, `${path}.content`),
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function toChatContent(value: unknown, path: string): unknown {
|
|
70
|
-
if (typeof value === "string") {
|
|
71
|
-
return value;
|
|
72
|
-
}
|
|
73
|
-
if (!Array.isArray(value)) {
|
|
74
|
-
throw new Error(`${path} must be a string or an array.`);
|
|
75
|
-
}
|
|
76
|
-
return value.map((rawPart, index) => {
|
|
77
|
-
const partPath = `${path}[${index}]`;
|
|
78
|
-
const part = requireRecord(rawPart, partPath);
|
|
79
|
-
const type = requireString(part.type, `${partPath}.type`);
|
|
80
|
-
if (type === "input_text") {
|
|
81
|
-
return {
|
|
82
|
-
type: "text",
|
|
83
|
-
text: requireString(part.text, `${partPath}.text`),
|
|
84
|
-
};
|
|
85
|
-
}
|
|
86
|
-
if (type === "input_image") {
|
|
87
|
-
return {
|
|
88
|
-
type: "image_url",
|
|
89
|
-
image_url: {
|
|
90
|
-
url: requireString(part.image_url, `${partPath}.image_url`),
|
|
91
|
-
...(part.detail === undefined
|
|
92
|
-
? {}
|
|
93
|
-
: { detail: requireString(part.detail, `${partPath}.detail`) }),
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
throw new Error(`${partPath}.type is unsupported: ${JSON.stringify(type)}.`);
|
|
98
|
-
});
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function appendFunctionCall(
|
|
102
|
-
messages: ChatEstimatorMessage[],
|
|
103
|
-
item: Record<string, unknown>,
|
|
104
|
-
path: string,
|
|
105
|
-
): void {
|
|
106
|
-
const call: ChatEstimatorToolCall = {
|
|
107
|
-
id: requireString(item.call_id, `${path}.call_id`),
|
|
108
|
-
type: "function",
|
|
109
|
-
function: {
|
|
110
|
-
name: requireString(item.name, `${path}.name`),
|
|
111
|
-
arguments: requireString(item.arguments, `${path}.arguments`),
|
|
112
|
-
},
|
|
113
|
-
};
|
|
114
|
-
const previous = messages[messages.length - 1];
|
|
115
|
-
if (previous?.role === "assistant") {
|
|
116
|
-
(previous.tool_calls ??= []).push(call);
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
messages.push({ role: "assistant", content: null, tool_calls: [call] });
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function toChatTools(value: unknown): Record<string, unknown>[] {
|
|
123
|
-
if (!Array.isArray(value)) {
|
|
124
|
-
throw new Error("Responses token estimator payload tools must be an array.");
|
|
125
|
-
}
|
|
126
|
-
return value.map((rawTool, index) => {
|
|
127
|
-
const path = `Responses token estimator tools[${index}]`;
|
|
128
|
-
const tool = requireRecord(rawTool, path);
|
|
129
|
-
if (tool.type !== "function") {
|
|
130
|
-
throw new Error(`${path}.type must be "function".`);
|
|
131
|
-
}
|
|
132
|
-
return {
|
|
133
|
-
type: "function",
|
|
134
|
-
function: {
|
|
135
|
-
name: requireString(tool.name, `${path}.name`),
|
|
136
|
-
description: requireString(tool.description, `${path}.description`),
|
|
137
|
-
parameters: requireRecord(tool.parameters, `${path}.parameters`),
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function requireRecord(value: unknown, path: string): Record<string, unknown> {
|
|
144
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
145
|
-
throw new Error(`${path} must be an object.`);
|
|
146
|
-
}
|
|
147
|
-
return value as Record<string, unknown>;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function requireString(value: unknown, path: string): string {
|
|
151
|
-
if (typeof value !== "string") {
|
|
152
|
-
throw new Error(`${path} must be a string.`);
|
|
153
|
-
}
|
|
154
|
-
return value;
|
|
155
|
-
}
|