-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathhistory.ts
More file actions
147 lines (136 loc) · 5.25 KB
/
Copy pathhistory.ts
File metadata and controls
147 lines (136 loc) · 5.25 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import type { Content } from "@google/genai";
import { assert } from "@std/assert";
import { encodeBase64 } from "@std/encoding";
import { normalizeToolName } from "../../tool.ts";
import type { ChatItem, ChatItemToolUse } from "../../types.ts";
import { serializeWrappedToolArguments } from "../shared/tools.ts";
import type { GoogleToolMap } from "./tools.ts";
type EnsureFileUploaded = (url: string, mimeType: string, abortSignal: AbortSignal) => Promise<string>;
const signatureMap = new Map<string, string>();
/**
* Google requires functionCall.args to be an object-like Struct, so replayed
* primitive tool inputs need wrapping when we no longer have the original schema.
*/
function normalizeGoogleFunctionCallArgs(content: string | undefined, tool: GoogleToolMap | undefined) {
if (!content) return undefined;
try {
const parsed = JSON.parse(serializeWrappedToolArguments(content, tool));
if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
return parsed;
}
return { content: parsed };
} catch {
return { content };
}
}
function getGoogleFileBaseUrl(url: string) {
return new URL("v1beta/files/", url.endsWith("/") ? url : `${url}/`).toString();
}
export function rememberGoogleThoughtSignature(toolUseId: string, signature: string) {
signatureMap.set(toolUseId, signature);
}
export async function getGoogleGenerateContentAPIHistory(options: {
history: ChatItem[];
toolMap: GoogleToolMap[];
signal: AbortSignal;
baseUrl?: string;
ensureFileUploaded?: EnsureFileUploaded;
/** Replay file history as inlineData parts instead of the Files API (unsupported on Vertex AI). */
inlineFiles?: boolean;
}): Promise<Content[]> {
const googleHistory: Content[] = [];
for (const item of options.history) {
switch (item.type) {
case "input_text":
googleHistory.push({ role: "user", parts: [{ text: item.content }] });
break;
case "output_text":
googleHistory.push({ role: "model", parts: [{ text: item.content }] });
break;
case "context_summary":
googleHistory.push({ role: "user", parts: [{ text: item.content }] });
break;
case "tool_use": {
const tool = options.toolMap.find((tool) => tool.original.name === item.kind);
// Magic word comes from https://ai.google.dev/gemini-api/docs/gemini-3?thinking=high#migrating_from_other_models
const thoughtSignature = signatureMap.get(item.tool_use_id) ?? "context_engineering_is_the_way_to_go";
googleHistory.push({
role: "model",
parts: [{
functionCall: {
id: item.tool_use_id,
name: tool?.google.name ?? normalizeToolName(item.kind),
args: normalizeGoogleFunctionCallArgs(item.content, tool),
},
thoughtSignature,
}],
});
break;
}
case "tool_result_text": {
const toolCall = options.history.find((candidate): candidate is ChatItemToolUse =>
candidate.type === "tool_use" &&
candidate.tool_use_id === item.tool_use_id
);
assert(toolCall, "Tool result is present in the history without initial tool call");
// We don't actually assert the definition's existence. Chat history might get reused without previously existing tool calls,
// e.g. for context compaction, or when user wants to implement custom tool selection system.
// The kind is enough to normalize to the original function name.
const definition = options.toolMap.find((tool) => tool.original.name === toolCall.kind);
googleHistory.push({
role: "user",
parts: [{
functionResponse: {
id: item.tool_use_id,
name: definition?.google.name ?? normalizeToolName(toolCall.kind),
response: { content: item.content },
},
}],
});
break;
}
case "input_file":
case "tool_result_file": {
if (options.inlineFiles) {
const response = await fetch(item.content, { signal: options.signal });
if (!response.ok) {
throw new Error(`Failed to fetch file for inline replay: ${response.status} ${item.content}`);
}
googleHistory.push({
role: "user",
parts: [{
inlineData: {
data: encodeBase64(await response.arrayBuffer()),
mimeType: item.kind,
},
}],
});
break;
}
if (!options.ensureFileUploaded) {
throw new Error("Google history file replay requires an upload handler");
}
const fileName = await options.ensureFileUploaded(item.content, item.kind, options.signal);
googleHistory.push({
role: "user",
parts: [{
fileData: {
fileUri: new URL(
fileName,
getGoogleFileBaseUrl(options.baseUrl ?? "https://generativelanguage.googleapis.com"),
).toString(),
mimeType: item.kind,
},
}],
});
break;
}
case "output_reasoning":
// no-op, don't propagate reasoning
break;
default:
item satisfies never;
}
}
return googleHistory;
}