mirror of https://github.com/openclaw/openclaw.git
fix(google): add direct cachedContent support (#60757)
* fix(google): restore gemini cache reporting * fix(google): split cli parsing into separate PR * fix(google): drop remaining cli overlap * fix(google): honor cachedContent alias precedence
This commit is contained in:
parent
b9e3c1a02e
commit
d766465e38
|
|
@ -75,6 +75,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Plugins/marketplace: block remote marketplace symlink escapes without breaking ordinary local marketplace install paths. (#60556) Thanks @eleqtrizit.
|
||||
- Plugins/install: preserve unsafe override flags across linked plugin and hook-pack probes so local `--link` installs honor the documented override behavior. (#60624) Thanks @JerrettDavis.
|
||||
- Config/All Settings: keep the raw config view intact when sensitive fields are blank instead of corrupting or dropping the rendered snapshot. (#28214) Thanks @solodmd.
|
||||
- Google/cache: pass explicit `cachedContent` handles through direct Google transport params and agent extra params so prebuilt Gemini context caches can be targeted again. Thanks @vincentkoc.
|
||||
- Security: preserve restrictive plugin-only tool allowlists, require owner access for `/allowlist add` and `/allowlist remove`, fail closed when `before_tool_call` hooks crash, block browser SSRF redirect bypasses earlier, and keep non-interactive auth-choice inference scoped to bundled and already-trusted plugins. (#58476, #59836, #59822, #58771, #59120)
|
||||
- Exec approvals: reuse durable exact-command `allow-always` approvals in allowlist mode so identical reruns stop prompting, and tighten Windows interpreter/path approval handling so wrapper and malformed-path cases fail closed more consistently. (#59880, #59780, #58040, #59182)
|
||||
- Agents/runtime: make default subagent allowlists, inherited skills/workspaces, and duplicate session-id resolution behave more predictably, and include value-shape hints in missing-parameter tool errors. (#59944, #59992, #59858, #55317)
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ describe("google transport stream", () => {
|
|||
} as unknown as Parameters<typeof streamFn>[1],
|
||||
{
|
||||
apiKey: "gemini-api-key",
|
||||
cachedContent: "cachedContents/request-cache",
|
||||
reasoning: "medium",
|
||||
toolChoice: "auto",
|
||||
} as Parameters<typeof streamFn>[2],
|
||||
|
|
@ -142,6 +143,7 @@ describe("google transport stream", () => {
|
|||
expect(payload.systemInstruction).toEqual({
|
||||
parts: [{ text: "Follow policy." }],
|
||||
});
|
||||
expect(payload.cachedContent).toBe("cachedContents/request-cache");
|
||||
expect(payload.generationConfig).toMatchObject({
|
||||
thinkingConfig: { includeThoughts: true, thinkingLevel: "HIGH" },
|
||||
});
|
||||
|
|
@ -246,4 +248,31 @@ describe("google transport stream", () => {
|
|||
thinkingConfig: { thinkingBudget: -1 },
|
||||
});
|
||||
});
|
||||
|
||||
it("includes cachedContent in direct Gemini payloads when requested", () => {
|
||||
const model = {
|
||||
id: "gemini-2.5-pro",
|
||||
name: "Gemini 2.5 Pro",
|
||||
api: "google-generative-ai",
|
||||
provider: "google",
|
||||
baseUrl: "https://generativelanguage.googleapis.com/v1beta",
|
||||
reasoning: true,
|
||||
input: ["text"],
|
||||
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
||||
contextWindow: 128000,
|
||||
maxTokens: 8192,
|
||||
} satisfies Model<"google-generative-ai">;
|
||||
|
||||
const params = buildGoogleGenerativeAiParams(
|
||||
model,
|
||||
{
|
||||
messages: [{ role: "user", content: "hello", timestamp: 0 }],
|
||||
} as never,
|
||||
{
|
||||
cachedContent: "cachedContents/prebuilt-context",
|
||||
},
|
||||
);
|
||||
|
||||
expect(params.cachedContent).toBe("cachedContents/prebuilt-context");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,6 +30,7 @@ type GoogleTransportModel = Model<"google-generative-ai"> & {
|
|||
type GoogleThinkingLevel = "MINIMAL" | "LOW" | "MEDIUM" | "HIGH";
|
||||
|
||||
type GoogleTransportOptions = SimpleStreamOptions & {
|
||||
cachedContent?: string;
|
||||
toolChoice?:
|
||||
| "auto"
|
||||
| "none"
|
||||
|
|
@ -49,6 +50,7 @@ type GoogleTransportOptions = SimpleStreamOptions & {
|
|||
};
|
||||
|
||||
type GoogleGenerateContentRequest = {
|
||||
cachedContent?: string;
|
||||
contents: Array<Record<string, unknown>>;
|
||||
generationConfig?: Record<string, unknown>;
|
||||
systemInstruction?: Record<string, unknown>;
|
||||
|
|
@ -441,6 +443,9 @@ export function buildGoogleGenerativeAiParams(
|
|||
const params: GoogleGenerateContentRequest = {
|
||||
contents: convertGoogleMessages(model, context),
|
||||
};
|
||||
if (typeof options?.cachedContent === "string" && options.cachedContent.trim()) {
|
||||
params.cachedContent = options.cachedContent.trim();
|
||||
}
|
||||
if (Object.keys(generationConfig).length > 0) {
|
||||
params.generationConfig = generationConfig;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,4 +37,71 @@ describe("extra-params: Google thinking payload compatibility", () => {
|
|||
expect(payload.config?.thinkingConfig?.thinkingBudget).toBeUndefined();
|
||||
expect(payload.config?.thinkingConfig?.thinkingLevel).toBe("HIGH");
|
||||
});
|
||||
|
||||
it("passes cachedContent through Google extra params", () => {
|
||||
const { options } = runExtraParamsCase({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
models: {
|
||||
"google/gemini-2.5-pro": {
|
||||
params: {
|
||||
cachedContent: "cachedContents/test-cache",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
applyProvider: "google",
|
||||
applyModelId: "gemini-2.5-pro",
|
||||
model: {
|
||||
api: "google-generative-ai",
|
||||
provider: "google",
|
||||
id: "gemini-2.5-pro",
|
||||
} as unknown as Model<"openai-completions">,
|
||||
payload: {
|
||||
contents: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect((options as { cachedContent?: string } | undefined)?.cachedContent).toBe(
|
||||
"cachedContents/test-cache",
|
||||
);
|
||||
});
|
||||
|
||||
it("lets higher-precedence cachedContent override lower-precedence cached_content", () => {
|
||||
const { options } = runExtraParamsCase({
|
||||
cfg: {
|
||||
agents: {
|
||||
defaults: {
|
||||
params: {
|
||||
cached_content: "cachedContents/default-cache",
|
||||
},
|
||||
models: {
|
||||
"google/gemini-2.5-pro": {
|
||||
params: {
|
||||
cachedContent: "cachedContents/model-cache",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
applyProvider: "google",
|
||||
applyModelId: "gemini-2.5-pro",
|
||||
model: {
|
||||
api: "google-generative-ai",
|
||||
provider: "google",
|
||||
id: "gemini-2.5-pro",
|
||||
} as unknown as Model<"openai-completions">,
|
||||
payload: {
|
||||
contents: [],
|
||||
},
|
||||
});
|
||||
|
||||
expect((options as { cachedContent?: string } | undefined)?.cachedContent).toBe(
|
||||
"cachedContents/model-cache",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { applyExtraParamsToAgent } from "./extra-params.js";
|
|||
|
||||
export type ExtraParamsCapture<TPayload extends Record<string, unknown>> = {
|
||||
headers?: Record<string, string>;
|
||||
options?: SimpleStreamOptions;
|
||||
payload: TPayload;
|
||||
};
|
||||
|
||||
|
|
@ -45,6 +46,7 @@ export function runExtraParamsCase<
|
|||
|
||||
const baseStreamFn: StreamFn = (model, _context, options) => {
|
||||
captured.headers = options?.headers;
|
||||
captured.options = options;
|
||||
options?.onPayload?.(params.payload, model);
|
||||
return createMockStream();
|
||||
};
|
||||
|
|
|
|||
|
|
@ -91,11 +91,22 @@ export function resolveExtraParams(params: {
|
|||
delete merged.textVerbosity;
|
||||
}
|
||||
|
||||
const resolvedCachedContent = resolveAliasedParamValue(
|
||||
[defaultParams, globalParams, agentParams],
|
||||
"cached_content",
|
||||
"cachedContent",
|
||||
);
|
||||
if (resolvedCachedContent !== undefined) {
|
||||
merged.cachedContent = resolvedCachedContent;
|
||||
delete merged.cached_content;
|
||||
}
|
||||
|
||||
return merged;
|
||||
}
|
||||
|
||||
type CacheRetentionStreamOptions = Partial<SimpleStreamOptions> & {
|
||||
cacheRetention?: "none" | "short" | "long";
|
||||
cachedContent?: string;
|
||||
openaiWsWarmup?: boolean;
|
||||
};
|
||||
type SupportedTransport = Exclude<CacheRetentionStreamOptions["transport"], undefined>;
|
||||
|
|
@ -137,6 +148,15 @@ export function resolvePreparedExtraParams(params: {
|
|||
...sanitizeExtraParamsRecord(resolvedExtraParams),
|
||||
...override,
|
||||
};
|
||||
const resolvedCachedContent = resolveAliasedParamValue(
|
||||
[resolvedExtraParams, override],
|
||||
"cached_content",
|
||||
"cachedContent",
|
||||
);
|
||||
if (resolvedCachedContent !== undefined) {
|
||||
merged.cachedContent = resolvedCachedContent;
|
||||
delete merged.cached_content;
|
||||
}
|
||||
return (
|
||||
providerRuntimeDeps.prepareProviderExtraParams({
|
||||
provider: params.provider,
|
||||
|
|
@ -207,6 +227,15 @@ function createStreamFnWithExtraParams(
|
|||
if (typeof extraParams.openaiWsWarmup === "boolean") {
|
||||
streamParams.openaiWsWarmup = extraParams.openaiWsWarmup;
|
||||
}
|
||||
const cachedContent =
|
||||
typeof extraParams.cachedContent === "string"
|
||||
? extraParams.cachedContent
|
||||
: typeof extraParams.cached_content === "string"
|
||||
? extraParams.cached_content
|
||||
: undefined;
|
||||
if (typeof cachedContent === "string" && cachedContent.trim()) {
|
||||
streamParams.cachedContent = cachedContent.trim();
|
||||
}
|
||||
const initialCacheRetention = resolveCacheRetention(
|
||||
extraParams,
|
||||
provider,
|
||||
|
|
|
|||
Loading…
Reference in New Issue