tinker-agent 1.0.65
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/README.md +173 -0
- package/package.json +78 -0
- package/patches/markdansi@0.3.2.patch +37 -0
- package/src/agent/context-builder.ts +43 -0
- package/src/agent/context-meter.ts +310 -0
- package/src/agent/loop.ts +525 -0
- package/src/agent/runtime-session.ts +1212 -0
- package/src/agent/session-ledger.ts +828 -0
- package/src/agent/turn-cancellation.ts +44 -0
- package/src/agent/types.ts +77 -0
- package/src/cli/config.ts +283 -0
- package/src/cli/index.ts +29 -0
- package/src/cli/model-profiles.ts +289 -0
- package/src/cli/run-runner.ts +107 -0
- package/src/cli/tui-runner.tsx +290 -0
- package/src/context/compiled-context-hash.ts +138 -0
- package/src/context/compiled-context-validator.ts +209 -0
- package/src/context/context-manager.ts +362 -0
- package/src/context/context-policy.ts +8 -0
- package/src/context/context-protocol-validator.ts +463 -0
- package/src/context/context-revision-compiler.ts +281 -0
- package/src/context/context-revision.ts +111 -0
- package/src/context/context-source.ts +30 -0
- package/src/context/context-swap-renderer.ts +272 -0
- package/src/context/protocol-frame.ts +240 -0
- package/src/context/swap-planner.ts +725 -0
- package/src/events/append-private-file.ts +16 -0
- package/src/events/bash-result-detail.ts +70 -0
- package/src/events/composite-event-sink.ts +82 -0
- package/src/events/event-sink.ts +16 -0
- package/src/events/jsonl-event-log.ts +13 -0
- package/src/events/observation-text-log.ts +195 -0
- package/src/events/stdout-event-printer.ts +396 -0
- package/src/events/types.ts +263 -0
- package/src/ids/runtime-id.ts +68 -0
- package/src/ids/uuid-v7.ts +5 -0
- package/src/instructions/project-instructions.ts +242 -0
- package/src/mcp/mcp-config.ts +144 -0
- package/src/mcp/mcp-manager.ts +216 -0
- package/src/mcp/mcp-tool-executor.ts +178 -0
- package/src/model/committed-prefix-auditor.ts +68 -0
- package/src/model/fake-model-client.ts +280 -0
- package/src/model/model-client.ts +64 -0
- package/src/model/model-context-profile.ts +134 -0
- package/src/model/model-request-preflight.ts +120 -0
- package/src/model/openai-chat-mapping.ts +444 -0
- package/src/model/openai-chat-model-client.ts +190 -0
- package/src/model/prompt-prefix-hash.ts +47 -0
- package/src/model/token-estimator.ts +148 -0
- package/src/observation/observation-builder.ts +481 -0
- package/src/session/resume-projection.ts +616 -0
- package/src/session/session-catalog.ts +270 -0
- package/src/session/session-errors.ts +121 -0
- package/src/session/session-history-reader.ts +535 -0
- package/src/session/session-lock.ts +291 -0
- package/src/session/session-schema.ts +741 -0
- package/src/session/session-store.ts +3067 -0
- package/src/session/sqlite-session-ledger.ts +153 -0
- package/src/tools/bash-task.ts +617 -0
- package/src/tools/bash.ts +450 -0
- package/src/tools/cwd-state.ts +22 -0
- package/src/tools/edit.ts +428 -0
- package/src/tools/file-diff.ts +116 -0
- package/src/tools/glob.ts +202 -0
- package/src/tools/grep.ts +550 -0
- package/src/tools/hash.ts +9 -0
- package/src/tools/path-safety.ts +33 -0
- package/src/tools/read.ts +319 -0
- package/src/tools/recall.ts +400 -0
- package/src/tools/registry.ts +213 -0
- package/src/tools/ripgrep.ts +220 -0
- package/src/tools/task-list.ts +59 -0
- package/src/tools/task-output-snapshot.ts +47 -0
- package/src/tools/task-output-tool.ts +62 -0
- package/src/tools/task-output.ts +159 -0
- package/src/tools/task-stop.ts +59 -0
- package/src/tools/task-tool-args.ts +29 -0
- package/src/tools/types.ts +330 -0
- package/src/tools/web-fetch/backend.ts +27 -0
- package/src/tools/web-fetch/browser-backend.ts +126 -0
- package/src/tools/web-fetch/exa-backend.ts +172 -0
- package/src/tools/web-fetch/index.ts +298 -0
- package/src/tools/web-fetch/local-backend.ts +267 -0
- package/src/tools/web-fetch/refiner.ts +78 -0
- package/src/tools/web-fetch/route.ts +95 -0
- package/src/tools/web-search.ts +300 -0
- package/src/tools/write.ts +244 -0
- package/src/tui/app.tsx +497 -0
- package/src/tui/components/assistant-markdown.tsx +47 -0
- package/src/tui/components/background-tasks.tsx +92 -0
- package/src/tui/components/bash-result-view.tsx +47 -0
- package/src/tui/components/context-status.tsx +127 -0
- package/src/tui/components/diff-view.tsx +151 -0
- package/src/tui/components/file-viewer.tsx +212 -0
- package/src/tui/components/footer.tsx +60 -0
- package/src/tui/components/header.tsx +21 -0
- package/src/tui/components/model-picker.tsx +142 -0
- package/src/tui/components/prompt-input.tsx +432 -0
- package/src/tui/components/resume-session-picker.tsx +273 -0
- package/src/tui/components/timeline.tsx +121 -0
- package/src/tui/context-format.ts +24 -0
- package/src/tui/event-store.ts +865 -0
- package/src/tui/git-branch.ts +23 -0
- package/src/tui/line-editor.ts +157 -0
- package/src/tui/prompt-history.ts +94 -0
- package/src/tui/slash-commands.ts +126 -0
- package/src/tui/tui-projection-policy.ts +35 -0
- package/src/tui/tui-projection-store.ts +123 -0
- package/src/tui/tui-session-controller.ts +170 -0
- package/src/tui/view-file.ts +122 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
export const TOKEN_K = 1_024;
|
|
2
|
+
export const PRODUCT_MAX_OUTPUT_TOKENS = 128 * TOKEN_K;
|
|
3
|
+
export const CONTEXT_PRESSURE_TRIGGER_RATIO = 0.8 as const;
|
|
4
|
+
|
|
5
|
+
export type ModelContextProfile = {
|
|
6
|
+
contextWindowTokens: number;
|
|
7
|
+
maxSupportedOutputTokens: number;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export type ModelContextBudget = ModelContextProfile & {
|
|
11
|
+
requestMaxOutputTokens: number;
|
|
12
|
+
inputBudgetTokens: number;
|
|
13
|
+
triggerRatio: typeof CONTEXT_PRESSURE_TRIGGER_RATIO;
|
|
14
|
+
triggerTokens: number;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function readModelContextProfileFromEnv(
|
|
18
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
19
|
+
): ModelContextProfile {
|
|
20
|
+
return createModelContextProfile({
|
|
21
|
+
contextWindowTokens: parseRequiredTokenCount(
|
|
22
|
+
env.TINKER_CONTEXT_WINDOW_TOKENS,
|
|
23
|
+
"TINKER_CONTEXT_WINDOW_TOKENS",
|
|
24
|
+
),
|
|
25
|
+
maxSupportedOutputTokens: parseRequiredTokenCount(
|
|
26
|
+
env.TINKER_MAX_SUPPORTED_OUTPUT_TOKENS,
|
|
27
|
+
"TINKER_MAX_SUPPORTED_OUTPUT_TOKENS",
|
|
28
|
+
),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function createModelContextProfile(
|
|
33
|
+
input: ModelContextProfile,
|
|
34
|
+
): ModelContextProfile {
|
|
35
|
+
requirePositiveSafeInteger(
|
|
36
|
+
input.contextWindowTokens,
|
|
37
|
+
"contextWindowTokens",
|
|
38
|
+
input.contextWindowTokens,
|
|
39
|
+
);
|
|
40
|
+
requirePositiveSafeInteger(
|
|
41
|
+
input.maxSupportedOutputTokens,
|
|
42
|
+
"maxSupportedOutputTokens",
|
|
43
|
+
input.maxSupportedOutputTokens,
|
|
44
|
+
);
|
|
45
|
+
if (input.maxSupportedOutputTokens > input.contextWindowTokens) {
|
|
46
|
+
throw new Error(
|
|
47
|
+
`maxSupportedOutputTokens must not exceed contextWindowTokens; received ${input.maxSupportedOutputTokens} > ${input.contextWindowTokens}.`,
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { ...input };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function deriveModelContextBudget(
|
|
55
|
+
profileInput: ModelContextProfile,
|
|
56
|
+
): ModelContextBudget {
|
|
57
|
+
const profile = createModelContextProfile(profileInput);
|
|
58
|
+
const requestMaxOutputTokens = Math.min(
|
|
59
|
+
PRODUCT_MAX_OUTPUT_TOKENS,
|
|
60
|
+
profile.maxSupportedOutputTokens,
|
|
61
|
+
);
|
|
62
|
+
if (requestMaxOutputTokens >= profile.contextWindowTokens) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`Derived requestMaxOutputTokens must be smaller than contextWindowTokens; received ${requestMaxOutputTokens} >= ${profile.contextWindowTokens}.`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const inputBudgetTokens = profile.contextWindowTokens - requestMaxOutputTokens;
|
|
69
|
+
if (inputBudgetTokens <= 0) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
`Derived inputBudgetTokens must be positive; received ${inputBudgetTokens}.`,
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const triggerTokens = Math.floor(inputBudgetTokens * CONTEXT_PRESSURE_TRIGGER_RATIO);
|
|
76
|
+
if (triggerTokens <= 0 || triggerTokens >= inputBudgetTokens) {
|
|
77
|
+
throw new Error(
|
|
78
|
+
`Derived triggerTokens must be between 0 and inputBudgetTokens; received ${triggerTokens} for budget ${inputBudgetTokens}.`,
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
...profile,
|
|
84
|
+
requestMaxOutputTokens,
|
|
85
|
+
inputBudgetTokens,
|
|
86
|
+
triggerRatio: CONTEXT_PRESSURE_TRIGGER_RATIO,
|
|
87
|
+
triggerTokens,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function assertMatchingContextBudget(
|
|
92
|
+
profile: ModelContextProfile,
|
|
93
|
+
budget: ModelContextBudget,
|
|
94
|
+
): void {
|
|
95
|
+
const expected = deriveModelContextBudget(profile);
|
|
96
|
+
for (const key of Object.keys(expected) as Array<keyof ModelContextBudget>) {
|
|
97
|
+
if (budget[key] !== expected[key]) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`Model context budget ${key} must be ${expected[key]}; received ${budget[key]}.`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseRequiredTokenCount(value: string | undefined, name: string): number {
|
|
106
|
+
if (value === undefined || value.trim() === "") {
|
|
107
|
+
throw new Error(`${name} is required; received ${displayValue(value)}.`);
|
|
108
|
+
}
|
|
109
|
+
if (!/^\d+$/.test(value)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
`${name} must be a positive safe integer token count; received ${displayValue(value)}.`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const parsed = Number(value);
|
|
116
|
+
requirePositiveSafeInteger(parsed, name, value);
|
|
117
|
+
return parsed;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function requirePositiveSafeInteger(
|
|
121
|
+
value: number,
|
|
122
|
+
name: string,
|
|
123
|
+
received: unknown,
|
|
124
|
+
): void {
|
|
125
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`${name} must be a positive safe integer token count; received ${displayValue(received)}.`,
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function displayValue(value: unknown): string {
|
|
133
|
+
return value === undefined ? "undefined" : JSON.stringify(value);
|
|
134
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { ModelContextBudget } from "./model-context-profile";
|
|
3
|
+
|
|
4
|
+
export type ContextUsageSource =
|
|
5
|
+
| "estimated_full"
|
|
6
|
+
| "provider_measured"
|
|
7
|
+
| "measured_plus_estimated_delta";
|
|
8
|
+
|
|
9
|
+
export type ContextPressure = "normal" | "triggered" | "blocked";
|
|
10
|
+
|
|
11
|
+
export class ContextBudgetExceededError extends Error {
|
|
12
|
+
readonly projectedInputTokens: number;
|
|
13
|
+
readonly inputBudgetTokens: number;
|
|
14
|
+
readonly triggerTokens: number;
|
|
15
|
+
readonly source: ContextUsageSource;
|
|
16
|
+
|
|
17
|
+
constructor(
|
|
18
|
+
input: {
|
|
19
|
+
projectedInputTokens: number;
|
|
20
|
+
source: ContextUsageSource;
|
|
21
|
+
} & Pick<
|
|
22
|
+
ModelContextBudget,
|
|
23
|
+
| "contextWindowTokens"
|
|
24
|
+
| "inputBudgetTokens"
|
|
25
|
+
| "requestMaxOutputTokens"
|
|
26
|
+
| "triggerTokens"
|
|
27
|
+
>,
|
|
28
|
+
) {
|
|
29
|
+
super(
|
|
30
|
+
`Model request blocked before provider call: projected input ${formatTokenCount(input.projectedInputTokens)} exceeds Tinker input budget ${formatTokenCount(input.inputBudgetTokens)} (model window ${formatTokenCount(input.contextWindowTokens)}, reserved output ${formatTokenCount(input.requestMaxOutputTokens)}).`,
|
|
31
|
+
);
|
|
32
|
+
this.name = "ContextBudgetExceededError";
|
|
33
|
+
this.projectedInputTokens = input.projectedInputTokens;
|
|
34
|
+
this.inputBudgetTokens = input.inputBudgetTokens;
|
|
35
|
+
this.triggerTokens = input.triggerTokens;
|
|
36
|
+
this.source = input.source;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function contextPressure(
|
|
41
|
+
usedInputTokens: number,
|
|
42
|
+
budget: Pick<ModelContextBudget, "inputBudgetTokens" | "triggerTokens">,
|
|
43
|
+
): ContextPressure {
|
|
44
|
+
if (usedInputTokens > budget.inputBudgetTokens) {
|
|
45
|
+
return "blocked";
|
|
46
|
+
}
|
|
47
|
+
return usedInputTokens >= budget.triggerTokens ? "triggered" : "normal";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function assertContextBudget(
|
|
51
|
+
input: {
|
|
52
|
+
usedInputTokens: number;
|
|
53
|
+
source: ContextUsageSource;
|
|
54
|
+
} & Pick<
|
|
55
|
+
ModelContextBudget,
|
|
56
|
+
| "contextWindowTokens"
|
|
57
|
+
| "inputBudgetTokens"
|
|
58
|
+
| "requestMaxOutputTokens"
|
|
59
|
+
| "triggerTokens"
|
|
60
|
+
>,
|
|
61
|
+
): void {
|
|
62
|
+
if (input.usedInputTokens <= input.inputBudgetTokens) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
throw new ContextBudgetExceededError({
|
|
66
|
+
projectedInputTokens: input.usedInputTokens,
|
|
67
|
+
source: input.source,
|
|
68
|
+
contextWindowTokens: input.contextWindowTokens,
|
|
69
|
+
inputBudgetTokens: input.inputBudgetTokens,
|
|
70
|
+
requestMaxOutputTokens: input.requestMaxOutputTokens,
|
|
71
|
+
triggerTokens: input.triggerTokens,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function stableJsonStringify(value: unknown): string {
|
|
76
|
+
return JSON.stringify(canonicalJsonValue(value));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function canonicalJsonValue(value: unknown): unknown {
|
|
80
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
if (typeof value === "number") {
|
|
84
|
+
if (!Number.isFinite(value)) {
|
|
85
|
+
throw new Error(`Cannot serialize a non-finite JSON number: ${value}.`);
|
|
86
|
+
}
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
if (Array.isArray(value)) {
|
|
90
|
+
return value.map((entry) => canonicalJsonValue(entry));
|
|
91
|
+
}
|
|
92
|
+
if (typeof value !== "object" || value === undefined) {
|
|
93
|
+
throw new Error(`Cannot serialize non-JSON value of type ${typeof value}.`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const record = value as Record<string, unknown>;
|
|
97
|
+
const canonical: Record<string, unknown> = {};
|
|
98
|
+
for (const key of Object.keys(record).sort()) {
|
|
99
|
+
const entry = record[key];
|
|
100
|
+
if (entry !== undefined) {
|
|
101
|
+
canonical[key] = canonicalJsonValue(entry);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return canonical;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function sha256(value: string): string {
|
|
108
|
+
return createHash("sha256").update(value, "utf8").digest("hex");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function formatTokenCount(tokens: number): string {
|
|
112
|
+
const million = 1_024 * 1_024;
|
|
113
|
+
if (tokens >= million && tokens % million === 0) {
|
|
114
|
+
return `${tokens / million}M`;
|
|
115
|
+
}
|
|
116
|
+
if (tokens >= 1_024 && tokens % 1_024 === 0) {
|
|
117
|
+
return `${tokens / 1_024}K`;
|
|
118
|
+
}
|
|
119
|
+
return tokens.toLocaleString("en-US");
|
|
120
|
+
}
|
|
@@ -0,0 +1,444 @@
|
|
|
1
|
+
import type { AgentMessage, IterationIdentity, ToolCall } from "../agent/types";
|
|
2
|
+
import type { RuntimeSessionContext } from "../agent/runtime-session";
|
|
3
|
+
import type { ModelRequestOutput, ModelUsage } from "./model-client";
|
|
4
|
+
import type { ToolDefinition } from "../tools/types";
|
|
5
|
+
import type {
|
|
6
|
+
ChatCompletionAssistantMessageParam,
|
|
7
|
+
ChatCompletionMessageFunctionToolCall,
|
|
8
|
+
ChatCompletionMessageParam,
|
|
9
|
+
ChatCompletionTool,
|
|
10
|
+
} from "openai/resources/chat/completions";
|
|
11
|
+
|
|
12
|
+
type DeepSeekAssistantMessageParam = ChatCompletionAssistantMessageParam & {
|
|
13
|
+
reasoning_content?: string | null;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type OpenAIChatMessageMappingOptions = {
|
|
17
|
+
includeReasoningContent?: boolean;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function toOpenAIChatMessages(
|
|
21
|
+
messages: readonly AgentMessage[],
|
|
22
|
+
options: OpenAIChatMessageMappingOptions = {},
|
|
23
|
+
): ChatCompletionMessageParam[] {
|
|
24
|
+
return messages.map((message): ChatCompletionMessageParam => {
|
|
25
|
+
if (message.role === "assistant") {
|
|
26
|
+
const assistantMessage: DeepSeekAssistantMessageParam = {
|
|
27
|
+
role: "assistant",
|
|
28
|
+
content: message.content ?? null,
|
|
29
|
+
tool_calls: message.toolCalls?.map(toOpenAIToolCall),
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
options.includeReasoningContent === true &&
|
|
34
|
+
message.reasoningContent !== undefined
|
|
35
|
+
) {
|
|
36
|
+
assistantMessage.reasoning_content = message.reasoningContent;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return assistantMessage;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (message.role === "tool") {
|
|
43
|
+
return {
|
|
44
|
+
role: "tool",
|
|
45
|
+
tool_call_id: message.providerToolCallId,
|
|
46
|
+
content: message.content,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return message;
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function toOpenAIChatTools(
|
|
55
|
+
tools: readonly ToolDefinition[],
|
|
56
|
+
): ChatCompletionTool[] {
|
|
57
|
+
return tools.map((tool) => ({
|
|
58
|
+
type: "function",
|
|
59
|
+
function: {
|
|
60
|
+
name: tool.name,
|
|
61
|
+
description: tool.description,
|
|
62
|
+
parameters: tool.parameters,
|
|
63
|
+
},
|
|
64
|
+
}));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function fromOpenAIChatCompletion(
|
|
68
|
+
response: unknown,
|
|
69
|
+
options: {
|
|
70
|
+
identity?: {
|
|
71
|
+
iteration: IterationIdentity;
|
|
72
|
+
runtimeSession: RuntimeSessionContext;
|
|
73
|
+
};
|
|
74
|
+
provider: string;
|
|
75
|
+
model: string;
|
|
76
|
+
},
|
|
77
|
+
): ModelRequestOutput {
|
|
78
|
+
const completion = requireRecord(response, "response", options);
|
|
79
|
+
if (!Array.isArray(completion.choices) || completion.choices.length === 0) {
|
|
80
|
+
throw providerResponseError(options, "choices[0]", "is missing");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const choice = requireRecord(completion.choices[0], "choices[0]", options);
|
|
84
|
+
const message = requireRecord(choice.message, "choices[0].message", options);
|
|
85
|
+
if (message.role !== "assistant") {
|
|
86
|
+
throw providerResponseError(
|
|
87
|
+
options,
|
|
88
|
+
"choices[0].message.role",
|
|
89
|
+
'must be "assistant"',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
if (message.tool_calls !== undefined && !Array.isArray(message.tool_calls)) {
|
|
93
|
+
throw providerResponseError(
|
|
94
|
+
options,
|
|
95
|
+
"choices[0].message.tool_calls",
|
|
96
|
+
"must be an array",
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const rawToolCalls = message.tool_calls ?? [];
|
|
100
|
+
const content = normalizeContent(
|
|
101
|
+
message.content,
|
|
102
|
+
"choices[0].message.content",
|
|
103
|
+
options,
|
|
104
|
+
);
|
|
105
|
+
if ((content === null || content.trim() === "") && rawToolCalls.length === 0) {
|
|
106
|
+
throw providerResponseError(
|
|
107
|
+
options,
|
|
108
|
+
"choices[0].message",
|
|
109
|
+
"has neither non-empty text nor tool calls",
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const finishReason = optionalString(
|
|
114
|
+
choice.finish_reason,
|
|
115
|
+
"choices[0].finish_reason",
|
|
116
|
+
options,
|
|
117
|
+
);
|
|
118
|
+
const usage = parseUsage(completion.usage, options);
|
|
119
|
+
|
|
120
|
+
if (rawToolCalls.length > 0 && options.identity === undefined) {
|
|
121
|
+
throw providerResponseError(
|
|
122
|
+
options,
|
|
123
|
+
"choices[0].message.tool_calls",
|
|
124
|
+
"requires an iteration identity context",
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
const toolCalls = rawToolCalls.map((raw, index) =>
|
|
128
|
+
parseToolCall(raw, index, options.identity!, options),
|
|
129
|
+
);
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
message: {
|
|
133
|
+
role: "assistant",
|
|
134
|
+
content,
|
|
135
|
+
reasoningContent: normalizeContent(
|
|
136
|
+
message.reasoning_content,
|
|
137
|
+
"choices[0].message.reasoning_content",
|
|
138
|
+
options,
|
|
139
|
+
),
|
|
140
|
+
toolCalls: toolCalls.length === 0 ? undefined : toolCalls,
|
|
141
|
+
},
|
|
142
|
+
finishReason,
|
|
143
|
+
usage,
|
|
144
|
+
rawResponse: response,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function toOpenAIToolCall(call: ToolCall): ChatCompletionMessageFunctionToolCall {
|
|
149
|
+
return {
|
|
150
|
+
id: call.providerToolCallId,
|
|
151
|
+
type: "function",
|
|
152
|
+
function: {
|
|
153
|
+
name: call.name,
|
|
154
|
+
arguments:
|
|
155
|
+
call.rawArgs ??
|
|
156
|
+
(typeof call.args === "string" ? call.args : JSON.stringify(call.args)),
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function parseToolCall(
|
|
162
|
+
raw: unknown,
|
|
163
|
+
index: number,
|
|
164
|
+
context: {
|
|
165
|
+
iteration: IterationIdentity;
|
|
166
|
+
runtimeSession: RuntimeSessionContext;
|
|
167
|
+
},
|
|
168
|
+
options: { provider: string; model: string },
|
|
169
|
+
): ToolCall {
|
|
170
|
+
const path = `choices[0].message.tool_calls[${index}]`;
|
|
171
|
+
const record = requireRecord(raw, path, options);
|
|
172
|
+
if (record.type !== "function") {
|
|
173
|
+
throw providerResponseError(options, `${path}.type`, 'must be "function"');
|
|
174
|
+
}
|
|
175
|
+
const providerToolCallId = requireNonEmptyString(record.id, `${path}.id`, options);
|
|
176
|
+
const fn = requireRecord(record.function, `${path}.function`, options);
|
|
177
|
+
const name = requireNonEmptyString(fn.name, `${path}.function.name`, options);
|
|
178
|
+
const rawArgs = requireString(fn.arguments, `${path}.function.arguments`, options);
|
|
179
|
+
let args: unknown = {};
|
|
180
|
+
let argsParseError: string | undefined;
|
|
181
|
+
|
|
182
|
+
if (rawArgs.trim() !== "") {
|
|
183
|
+
try {
|
|
184
|
+
args = JSON.parse(rawArgs);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
argsParseError = error instanceof Error ? error.message : String(error);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
...context.runtimeSession.createToolCall(context.iteration, index + 1),
|
|
192
|
+
providerToolCallId,
|
|
193
|
+
name,
|
|
194
|
+
args,
|
|
195
|
+
rawArgs,
|
|
196
|
+
argsParseError,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function normalizeContent(
|
|
201
|
+
content: unknown,
|
|
202
|
+
path: string,
|
|
203
|
+
options: { provider: string; model: string },
|
|
204
|
+
): string | null {
|
|
205
|
+
if (content === null || content === undefined) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (typeof content === "string") {
|
|
210
|
+
return content;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
throw providerResponseError(options, path, "must be a string or null");
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
217
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
218
|
+
? (value as Record<string, unknown>)
|
|
219
|
+
: {};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function requireRecord(
|
|
223
|
+
value: unknown,
|
|
224
|
+
path: string,
|
|
225
|
+
options: { provider: string; model: string },
|
|
226
|
+
): Record<string, unknown> {
|
|
227
|
+
const record = asRecord(value);
|
|
228
|
+
if (Object.keys(record).length === 0) {
|
|
229
|
+
throw providerResponseError(options, path, "must be a non-empty object");
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return record;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function requireNonEmptyString(
|
|
236
|
+
value: unknown,
|
|
237
|
+
path: string,
|
|
238
|
+
options: { provider: string; model: string },
|
|
239
|
+
): string {
|
|
240
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
241
|
+
throw providerResponseError(options, path, "must be a non-empty string");
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return value;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function requireString(
|
|
248
|
+
value: unknown,
|
|
249
|
+
path: string,
|
|
250
|
+
options: { provider: string; model: string },
|
|
251
|
+
): string {
|
|
252
|
+
if (typeof value !== "string") {
|
|
253
|
+
throw providerResponseError(options, path, "must be a string");
|
|
254
|
+
}
|
|
255
|
+
return value;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function optionalString(
|
|
259
|
+
value: unknown,
|
|
260
|
+
path: string,
|
|
261
|
+
options: { provider: string; model: string },
|
|
262
|
+
): string | undefined {
|
|
263
|
+
if (value === null || value === undefined) {
|
|
264
|
+
return undefined;
|
|
265
|
+
}
|
|
266
|
+
return requireString(value, path, options);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function parseUsage(
|
|
270
|
+
value: unknown,
|
|
271
|
+
options: { provider: string; model: string },
|
|
272
|
+
): ModelUsage {
|
|
273
|
+
if (value === undefined || value === null) {
|
|
274
|
+
throw providerResponseError(options, "usage", "is required");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const usage = requireRecord(value, "usage", options);
|
|
278
|
+
const promptTokens = requireTokenCount(
|
|
279
|
+
usage.prompt_tokens,
|
|
280
|
+
"usage.prompt_tokens",
|
|
281
|
+
options,
|
|
282
|
+
);
|
|
283
|
+
const completionTokens = requireTokenCount(
|
|
284
|
+
usage.completion_tokens,
|
|
285
|
+
"usage.completion_tokens",
|
|
286
|
+
options,
|
|
287
|
+
);
|
|
288
|
+
const totalTokens = requireTokenCount(
|
|
289
|
+
usage.total_tokens,
|
|
290
|
+
"usage.total_tokens",
|
|
291
|
+
options,
|
|
292
|
+
);
|
|
293
|
+
if (totalTokens !== promptTokens + completionTokens) {
|
|
294
|
+
throw providerResponseError(
|
|
295
|
+
options,
|
|
296
|
+
"usage.total_tokens",
|
|
297
|
+
`must equal usage.prompt_tokens + usage.completion_tokens (${promptTokens + completionTokens})`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const directHit = optionalTokenCount(
|
|
302
|
+
usage.prompt_cache_hit_tokens,
|
|
303
|
+
"usage.prompt_cache_hit_tokens",
|
|
304
|
+
options,
|
|
305
|
+
);
|
|
306
|
+
const directMiss = optionalTokenCount(
|
|
307
|
+
usage.prompt_cache_miss_tokens,
|
|
308
|
+
"usage.prompt_cache_miss_tokens",
|
|
309
|
+
options,
|
|
310
|
+
);
|
|
311
|
+
if ((directHit === undefined) !== (directMiss === undefined)) {
|
|
312
|
+
throw providerResponseError(
|
|
313
|
+
options,
|
|
314
|
+
"usage.prompt_cache_hit_tokens",
|
|
315
|
+
"and usage.prompt_cache_miss_tokens must be provided together",
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const cachedTokens = parseNestedTokenCount(
|
|
320
|
+
usage.prompt_tokens_details,
|
|
321
|
+
"usage.prompt_tokens_details",
|
|
322
|
+
"cached_tokens",
|
|
323
|
+
options,
|
|
324
|
+
);
|
|
325
|
+
const detailHit = cachedTokens;
|
|
326
|
+
const detailMiss =
|
|
327
|
+
cachedTokens === undefined ? undefined : promptTokens - cachedTokens;
|
|
328
|
+
if (detailMiss !== undefined && detailMiss < 0) {
|
|
329
|
+
throw providerResponseError(
|
|
330
|
+
options,
|
|
331
|
+
"usage.prompt_tokens_details.cached_tokens",
|
|
332
|
+
"must not exceed usage.prompt_tokens",
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
if (
|
|
336
|
+
directHit !== undefined &&
|
|
337
|
+
(directHit !== detailHit || directMiss !== detailMiss) &&
|
|
338
|
+
detailHit !== undefined
|
|
339
|
+
) {
|
|
340
|
+
throw providerResponseError(
|
|
341
|
+
options,
|
|
342
|
+
"usage.prompt_cache_hit_tokens",
|
|
343
|
+
"conflicts with usage.prompt_tokens_details.cached_tokens",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const promptCacheHitTokens = directHit ?? detailHit;
|
|
348
|
+
const promptCacheMissTokens = directMiss ?? detailMiss;
|
|
349
|
+
if (
|
|
350
|
+
promptCacheHitTokens !== undefined &&
|
|
351
|
+
promptCacheMissTokens !== undefined &&
|
|
352
|
+
promptCacheHitTokens + promptCacheMissTokens !== promptTokens
|
|
353
|
+
) {
|
|
354
|
+
throw providerResponseError(
|
|
355
|
+
options,
|
|
356
|
+
"usage.prompt_cache_hit_tokens",
|
|
357
|
+
"plus usage.prompt_cache_miss_tokens must equal usage.prompt_tokens",
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const reasoningTokens = parseNestedTokenCount(
|
|
362
|
+
usage.completion_tokens_details,
|
|
363
|
+
"usage.completion_tokens_details",
|
|
364
|
+
"reasoning_tokens",
|
|
365
|
+
options,
|
|
366
|
+
);
|
|
367
|
+
if (reasoningTokens !== undefined && reasoningTokens > completionTokens) {
|
|
368
|
+
throw providerResponseError(
|
|
369
|
+
options,
|
|
370
|
+
"usage.completion_tokens_details.reasoning_tokens",
|
|
371
|
+
"must not exceed usage.completion_tokens",
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
promptTokens,
|
|
377
|
+
completionTokens,
|
|
378
|
+
totalTokens,
|
|
379
|
+
...(promptCacheHitTokens === undefined
|
|
380
|
+
? {}
|
|
381
|
+
: { promptCacheHitTokens, promptCacheMissTokens }),
|
|
382
|
+
...(reasoningTokens === undefined ? {} : { reasoningTokens }),
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function requireTokenCount(
|
|
387
|
+
value: unknown,
|
|
388
|
+
path: string,
|
|
389
|
+
options: { provider: string; model: string },
|
|
390
|
+
): number {
|
|
391
|
+
const count = optionalTokenCount(value, path, options);
|
|
392
|
+
if (count === undefined) {
|
|
393
|
+
throw providerResponseError(options, path, "is required");
|
|
394
|
+
}
|
|
395
|
+
return count;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function parseNestedTokenCount(
|
|
399
|
+
value: unknown,
|
|
400
|
+
path: string,
|
|
401
|
+
property: string,
|
|
402
|
+
options: { provider: string; model: string },
|
|
403
|
+
): number | undefined {
|
|
404
|
+
if (value === undefined || value === null) {
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
const record = requireObject(value, path, options);
|
|
408
|
+
return optionalTokenCount(record[property], `${path}.${property}`, options);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function requireObject(
|
|
412
|
+
value: unknown,
|
|
413
|
+
path: string,
|
|
414
|
+
options: { provider: string; model: string },
|
|
415
|
+
): Record<string, unknown> {
|
|
416
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
417
|
+
throw providerResponseError(options, path, "must be an object");
|
|
418
|
+
}
|
|
419
|
+
return value as Record<string, unknown>;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function optionalTokenCount(
|
|
423
|
+
value: unknown,
|
|
424
|
+
path: string,
|
|
425
|
+
options: { provider: string; model: string },
|
|
426
|
+
): number | undefined {
|
|
427
|
+
if (value === undefined) {
|
|
428
|
+
return undefined;
|
|
429
|
+
}
|
|
430
|
+
if (!Number.isSafeInteger(value) || (value as number) < 0) {
|
|
431
|
+
throw providerResponseError(options, path, "must be a non-negative integer");
|
|
432
|
+
}
|
|
433
|
+
return value as number;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function providerResponseError(
|
|
437
|
+
options: { provider: string; model: string },
|
|
438
|
+
path: string,
|
|
439
|
+
detail: string,
|
|
440
|
+
): Error {
|
|
441
|
+
return new Error(
|
|
442
|
+
`Invalid provider response (provider=${options.provider}, model=${options.model}): ${path} ${detail}.`,
|
|
443
|
+
);
|
|
444
|
+
}
|