Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@alphaxiv/agents",
"version": "0.7.2",
"version": "0.7.3",
"license": "MIT",
"fmt": {
"lineWidth": 120
Expand Down
6 changes: 4 additions & 2 deletions src/adapters/google_genai/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export function googleGenerateContentAPIModel<zO, zI>(
const client = new GoogleGenAI(options?.googleGenAIOptions ?? {});
const baseUrl = options.googleGenAIOptions?.httpOptions?.baseUrl ?? "https://generativelanguage.googleapis.com";
const thinkingConfig = options.thinkingConfig;
const provider = options.provider ?? "GoogleGenerateContentAPI";

async function ensureFileUploaded(url: string, mimeType: string, abortSignal: AbortSignal): Promise<string> {
// Create a safe filename by hashing the URL
Expand Down Expand Up @@ -102,7 +103,7 @@ export function googleGenerateContentAPIModel<zO, zI>(
}

return {
provider: options.provider ?? "GoogleGenerateContentAPI",
provider,
model: options.model,
stream: async function* stream<zO, zI>({
history,
Expand All @@ -113,6 +114,7 @@ export function googleGenerateContentAPIModel<zO, zI>(
}: AdapterStreamOptions<zO, zI>): AdapterStreamIterator {
const normalizedTools = normalizeGoogleTools(tools);
const googleHistory = await getGoogleGenerateContentAPIHistory({
provider,
history,
toolMap: normalizedTools,
signal,
Expand Down Expand Up @@ -170,7 +172,7 @@ export function googleGenerateContentAPIModel<zO, zI>(
const tool = normalizedTools.find((tool) => tool.google.name === func.name);

if (part.thoughtSignature) {
rememberGoogleThoughtSignature(funcId, part.thoughtSignature);
rememberGoogleThoughtSignature(provider, funcId, part.thoughtSignature);
}

const index = advanceIndex(funcId);
Expand Down
17 changes: 12 additions & 5 deletions src/adapters/google_genai/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import type { GoogleToolMap } from "./tools.ts";

type EnsureFileUploaded = (url: string, mimeType: string, abortSignal: AbortSignal) => Promise<string>;

const signatureMap = new Map<string, string>();
// A signature is only valid on the endpoint that issued it, so a replay elsewhere sends the placeholder instead.
const signatureMap = new Map<string, { provider: string; signature: string }>();

// Magic word comes from https://ai.google.dev/gemini-api/docs/gemini-3?thinking=high#migrating_from_other_models
const PLACEHOLDER_THOUGHT_SIGNATURE = "context_engineering_is_the_way_to_go";

/**
* Google requires functionCall.args to be an object-like Struct, so replayed
Expand All @@ -31,11 +35,12 @@ 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 function rememberGoogleThoughtSignature(provider: string, toolUseId: string, signature: string) {
signatureMap.set(toolUseId, { provider, signature });
}

export async function getGoogleGenerateContentAPIHistory(options: {
provider: string;
history: ChatItem[];
toolMap: GoogleToolMap[];
signal: AbortSignal;
Expand All @@ -58,8 +63,10 @@ export async function getGoogleGenerateContentAPIHistory(options: {
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";
const remembered = signatureMap.get(item.tool_use_id);
const thoughtSignature = remembered?.provider === options.provider
? remembered.signature
: PLACEHOLDER_THOUGHT_SIGNATURE;

googleHistory.push({
role: "model",
Expand Down
31 changes: 30 additions & 1 deletion tests/adapters/gemini.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import { assertEquals } from "@std/assert";
import { encodeBase64 } from "@std/encoding";
import { geminiModel } from "../../src/adapters/gemini/adapter.ts";
import { getThinkingConfig, googleGenerateContentAPIModel } from "../../src/adapters/google_genai/adapter.ts";
import { getGoogleGenerateContentAPIHistory } from "../../src/adapters/google_genai/history.ts";
import {
getGoogleGenerateContentAPIHistory,
rememberGoogleThoughtSignature,
} from "../../src/adapters/google_genai/history.ts";
import {
createToolFixtures,
INTEGRATION_TIMEOUT_MS,
Expand Down Expand Up @@ -407,6 +410,7 @@ Deno.test({

Deno.test("GeminiAdapter replays tool calls and results without current tool definitions", async () => {
const history = await getGoogleGenerateContentAPIHistory({
provider: "Gemini",
history: [
{ type: "tool_use", tool_use_id: "call_1", kind: "Load Project Snapshot", content: undefined },
{ type: "tool_result_text", tool_use_id: "call_1", content: "ready" },
Expand Down Expand Up @@ -440,8 +444,30 @@ Deno.test("GeminiAdapter replays tool calls and results without current tool def
]);
});

Deno.test("GeminiAdapter replays a thought signature only to the endpoint that issued it", async () => {
rememberGoogleThoughtSignature("Vertex AI", "call_1", "vertex-signature");
const history = [{ type: "tool_use" as const, tool_use_id: "call_1", kind: "Search Papers", content: undefined }];

const vertex = await getGoogleGenerateContentAPIHistory({
provider: "Vertex AI",
history,
toolMap: [],
signal: AbortSignal.abort(),
});
assertEquals(vertex[0].parts?.[0].thoughtSignature, "vertex-signature");

const gemini = await getGoogleGenerateContentAPIHistory({
provider: "Gemini",
history,
toolMap: [],
signal: AbortSignal.abort(),
});
assertEquals(gemini[0].parts?.[0].thoughtSignature, "context_engineering_is_the_way_to_go");
});

Deno.test("GeminiAdapter drops a tool result whose call is missing from the history", async () => {
const history = await getGoogleGenerateContentAPIHistory({
provider: "Gemini",
history: [
{ type: "tool_result_text", tool_use_id: "call_1", content: "orphaned result" },
{ type: "input_text", content: "what did you find?" },
Expand All @@ -455,6 +481,7 @@ Deno.test("GeminiAdapter drops a tool result whose call is missing from the hist

Deno.test("GeminiAdapter wraps primitive tool arguments when replaying without current tool definitions", async () => {
const history = await getGoogleGenerateContentAPIHistory({
provider: "Gemini",
history: [
{
type: "tool_use",
Expand Down Expand Up @@ -484,6 +511,7 @@ Deno.test("GeminiAdapter wraps primitive tool arguments when replaying without c

Deno.test("GeminiAdapter replays mixed tool history for a tool-less handoff", async () => {
const history = await getGoogleGenerateContentAPIHistory({
provider: "Gemini",
history: [
{
type: "tool_use",
Expand Down Expand Up @@ -605,6 +633,7 @@ Deno.test("GeminiAdapter inlines file history instead of using the Files API", a
const server = Deno.serve({ port: 0, onListen: () => {} }, () => new Response(bytes));
try {
const history = await getGoogleGenerateContentAPIHistory({
provider: "Gemini",
history: [{
type: "input_file",
kind: "application/pdf",
Expand Down
Loading