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
79 changes: 79 additions & 0 deletions src/compress-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -610,3 +610,82 @@ export const ABSORB_TOOL_OPENAI = {
parameters: ABSORB_PARAMETERS,
},
};

// Gemini native format: flat `{ name, description, parameters }` function
// declarations (no `{type:"function"}` wrapper), injected as
// `tools[].functionDeclarations`.
export const COMPRESS_TOOL_GOOGLE = {
name: COMPRESS_TOOL_NAME,
description: COMPRESS_TOOL.description,
// `anyOf` is rejected by older API revisions, so `content` declares the
// object form; a JSON-encoded string of that array is still accepted by
// parseCompressInput, and the line form (a summary whose first line is
// 'm00150–m00220 optional topic') is documented in the description.
parameters: {
type: "object",
properties: {
topic: {
type: "string",
description: "Optional short title for the compressed range",
},
content: {
type: "array",
description:
"One or more ranges to compress into separate summary blocks. Object form: {startId,endId,summary,topic?}. A JSON-encoded string of that array is also accepted; in the line form the summary begins with its own first line 'm00150–m00220 optional topic', the rest being the summary markdown verbatim. REQUIRED — compress without content is invalid.",
items: {
type: "object",
properties: {
topic: { type: "string" },
startId: {
type: "string",
description: "mNNNNN ref at the start of the range",
},
endId: {
type: "string",
description: "mNNNNN ref at the end of the range",
},
summary: {
type: "string",
description: "Self-contained summary replacing the range",
},
},
required: ["startId", "endId", "summary"],
},
},
},
required: ["content"],
},
};

export const DECOMPRESS_TOOL_GOOGLE = {
name: DECOMPRESS_TOOL_NAME,
description: DECOMPRESS_TOOL_OPENAI.function.description,
parameters: DECOMPRESS_TOOL_OPENAI.function.parameters,
};

export const SEARCH_CONTEXT_TOOL_GOOGLE = {
name: SEARCH_CONTEXT_TOOL_NAME,
description: SEARCH_CONTEXT_TOOL_OPENAI.function.description,
parameters: SEARCH_CONTEXT_TOOL_OPENAI.function.parameters,
};

export const ACP_STATUS_TOOL_GOOGLE = {
name: ACP_STATUS_TOOL_NAME,
description: ACP_STATUS_TOOL_OPENAI.function.description,
parameters: ACP_STATUS_TOOL_OPENAI.function.parameters,
};

/** All ACP tools in Gemini flat format, matching ACP_TOOL_NAMES. */
export const ACP_TOOLS_GOOGLE = [
COMPRESS_TOOL_GOOGLE,
DECOMPRESS_TOOL_GOOGLE,
SEARCH_CONTEXT_TOOL_GOOGLE,
ACP_STATUS_TOOL_GOOGLE,
] as const;

/** Opt-in absorb tool in Gemini flat format (config.absorb.enabled). */
export const ABSORB_TOOL_GOOGLE = {
name: ABSORB_TOOL_NAME,
description: ABSORB_TOOL_DESCRIPTION,
parameters: ABSORB_PARAMETERS,
};
10 changes: 10 additions & 0 deletions src/wire/bili-message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,20 @@ export interface BiliMessage extends CoreMessage {
/** Responses API: the original input item (for input_image, or a raw
* function_call / function_call_output we pass through). */
rawResponsesItem?: unknown;
/** Google/Gemini: the original content parts of this message, in wire
* order (inlineData/fileData, executableCode, functionCall/Response ids,
* thought parts and unknown future part types). coreToGoogle rebuilds
* the content from these when present, so parts the core does not model
* survive a round-trip. */
rawGoogleParts?: unknown[];
/** Anthropic thinking signature. Anthropic verifies thinking+signature
* pairs; without it the request is rejected. Stored alongside the
* reasoning text so coreToAnthropic can reattach it. */
thinkingSignature?: string;
/** Google/Gemini thought signature (thoughtSignature). Gemini 3 validates
* the signature of every replayed thought part, so a reasoning core
* carries it here for coreToGoogle to reattach. */
googleThoughtSignature?: string;
/** OpenAI reasoning_content (chain-of-thought from DeepSeek-R1, GLM-4.6
* thinking, Qwen-QwQ). These models require reasoning_content be echoed
* back on subsequent requests or the API returns HTTP 400; stored so
Expand Down
5 changes: 4 additions & 1 deletion src/wire/formats.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export const WIRE_FORMATS = ["anthropic", "openai", "responses"] as const;
export const WIRE_FORMATS = ["anthropic", "openai", "responses", "google"] as const;
export type WireFormat = (typeof WIRE_FORMATS)[number];

export function isWireFormat(value: unknown): value is WireFormat {
Expand All @@ -16,6 +16,9 @@ export function isWireFormat(value: unknown): value is WireFormat {
export function detectWireFormat(payload: unknown): WireFormat | undefined {
if (payload === null || typeof payload !== "object") return undefined;
const p = payload as Record<string, unknown>;
// Gemini native carries the conversation in `contents`; no other dialect
// does, and it never has `input`/`messages`, so the check is unambiguous.
if (Array.isArray(p.contents)) return "google";
if (Array.isArray(p.input)) return "responses";
const messages = p.messages;
if (!Array.isArray(messages)) return undefined;
Expand Down
Loading
Loading