-
Notifications
You must be signed in to change notification settings - Fork 0
feat(google): native Google Search grounding on the v2026.5.22 train #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
843893f
5038569
567dfaa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| name: Cut Dojo fork tarball | ||
|
|
||
| # One-shot: build the fork on a CI runner (16GB) and publish the tarball as a | ||
| # GitHub Release, so the DojoOS gateway Dockerfile can pin it. Replaces the | ||
| # manual `npm pack` that OOMs on low-RAM laptops. | ||
|
|
||
| on: | ||
| workflow_dispatch: | ||
| inputs: | ||
| tag: | ||
| description: 'Release tag (e.g. v2026.5.22-dojo.3)' | ||
| required: true | ||
| notes: | ||
| description: 'Release notes (one line)' | ||
| required: false | ||
| default: 'Dojo fork tarball' | ||
|
|
||
| jobs: | ||
| cut: | ||
| runs-on: ubuntu-24.04 | ||
| permissions: | ||
| contents: write | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: ./.github/actions/setup-node-env | ||
| with: | ||
| install-bun: "false" | ||
| - name: Build fork + Control UI | ||
| run: | | ||
| pnpm build | ||
| pnpm ui:build | ||
| - name: Pack tarball | ||
| id: pack | ||
| run: | | ||
| npm pack | ||
| TGZ="$(ls openclaw-*.tgz)" | ||
| echo "tgz=$TGZ" >> "$GITHUB_OUTPUT" | ||
| echo "SHA256:"; sha256sum "$TGZ" | ||
| - name: Publish release | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| run: | | ||
| gh release create "${{ inputs.tag }}" "${{ steps.pack.outputs.tgz }}" \ | ||
| --title "${{ inputs.tag }}" \ | ||
| --notes "${{ inputs.notes }}" | ||
| echo "SHA256 for the plugin Dockerfile ARG:" | ||
| sha256sum "${{ steps.pack.outputs.tgz }}" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -107,10 +107,29 @@ type MutableAssistantOutput = { | |
| timestamp: number; | ||
| responseId?: string; | ||
| errorMessage?: string; | ||
| webGrounding?: TransportWebGrounding; | ||
| }; | ||
|
|
||
| const GOOGLE_VERTEX_DEFAULT_API_VERSION = "v1"; | ||
|
|
||
| type GoogleGroundingMetadata = { | ||
| webSearchQueries?: string[]; | ||
| groundingChunks?: Array<{ | ||
| web?: { title?: string; uri?: string; domain?: string }; | ||
| }>; | ||
| groundingSupports?: Array<{ groundingChunkIndices?: number[] }>; | ||
| }; | ||
|
|
||
| /** | ||
| * Normalized Google Search grounding payload attached to the assistant | ||
| * output message. Consumers (gateway plugins, the OpenAI-compat endpoint) | ||
| * read it as `(message as { webGrounding?: TransportWebGrounding })`. | ||
| */ | ||
| export type TransportWebGrounding = { | ||
| queries: string[]; | ||
| sources: Array<{ title: string; url: string; cited?: number }>; | ||
| }; | ||
|
|
||
| type GoogleSseChunk = { | ||
| responseId?: string; | ||
| candidates?: Array<{ | ||
|
|
@@ -127,6 +146,7 @@ type GoogleSseChunk = { | |
| }>; | ||
| }; | ||
| finishReason?: string; | ||
| groundingMetadata?: GoogleGroundingMetadata; | ||
| }>; | ||
| usageMetadata?: { | ||
| promptTokenCount?: number; | ||
|
|
@@ -690,6 +710,59 @@ function convertGoogleTools(tools: NonNullable<Context["tools"]>) { | |
| ]; | ||
| } | ||
|
|
||
| /** | ||
| * Server-side Google Search grounding (opt-in via env). When enabled the | ||
| * request carries the `google_search` built-in tool; mixing it with | ||
| * `functionDeclarations` additionally requires | ||
| * `toolConfig.includeServerSideToolInvocations` (the API 400s without it). | ||
| * The resulting `groundingMetadata` is normalized onto the assistant output | ||
| * as `webGrounding`. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔵 P3 (minor) — Potential duplication and inflated citation counts during streaming re-entries. If Since the [pass 1] |
||
| */ | ||
| function isGoogleSearchGroundingEnabled(): boolean { | ||
| const raw = process.env.OPENCLAW_GOOGLE_GROUNDING?.trim().toLowerCase(); | ||
| return raw === "1" || raw === "true" || raw === "on"; | ||
| } | ||
|
|
||
| function mergeWebGrounding( | ||
| output: MutableAssistantOutput, | ||
| metadata: GoogleGroundingMetadata, | ||
| ): void { | ||
| const grounding = (output.webGrounding ??= { queries: [], sources: [] }); | ||
| for (const query of metadata.webSearchQueries ?? []) { | ||
| if (typeof query === "string" && query && !grounding.queries.includes(query)) { | ||
| grounding.queries.push(query); | ||
| } | ||
| } | ||
| // Citation counts per chunk index (`groundingSupports`) let consumers rank | ||
| // sources by how often the answer actually cited them. | ||
| const citeCounts = new Map<number, number>(); | ||
| for (const support of metadata.groundingSupports ?? []) { | ||
| for (const idx of support?.groundingChunkIndices ?? []) { | ||
| if (typeof idx === "number") { | ||
| citeCounts.set(idx, (citeCounts.get(idx) ?? 0) + 1); | ||
| } | ||
| } | ||
| } | ||
| const chunks = metadata.groundingChunks ?? []; | ||
| for (let idx = 0; idx < chunks.length; idx++) { | ||
| const chunk = chunks[idx]; | ||
| const url = chunk?.web?.uri; | ||
| if (typeof url !== "string" || !url) { | ||
| continue; | ||
| } | ||
| const existing = grounding.sources.find((source) => source.url === url); | ||
| if (existing) { | ||
| existing.cited = (existing.cited ?? 0) + (citeCounts.get(idx) ?? 0); | ||
| continue; | ||
| } | ||
| grounding.sources.push({ | ||
| title: chunk.web?.title || chunk.web?.domain || url, | ||
| url, | ||
| cited: citeCounts.get(idx) ?? 0, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| export function buildGoogleGenerativeAiParams( | ||
| model: GoogleTransportModel, | ||
| context: Context, | ||
|
|
@@ -734,6 +807,15 @@ export function buildGoogleGenerativeAiParams( | |
| }; | ||
| } | ||
| } | ||
| if (isGoogleSearchGroundingEnabled()) { | ||
| params.tools = [...(params.tools ?? []), { google_search: {} }]; | ||
| if (context.tools?.length) { | ||
| params.toolConfig = { | ||
| ...params.toolConfig, | ||
| includeServerSideToolInvocations: true, | ||
| }; | ||
| } | ||
| } | ||
| return params; | ||
| } | ||
|
|
||
|
|
@@ -1241,6 +1323,9 @@ function createGoogleTransportStreamFn(kind: CanonicalGoogleTransportApi): Strea | |
| output.responseId ||= chunk.responseId; | ||
| updateUsage(output, model, chunk); | ||
| const candidate = chunk.candidates?.[0]; | ||
| if (candidate?.groundingMetadata) { | ||
| mergeWebGrounding(output, candidate.groundingMetadata); | ||
| } | ||
| if (candidate?.content?.parts) { | ||
| for (const part of candidate.content.parts) { | ||
| const hasThoughtSignature = | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 P2 (major) — Shell injection and quoting vulnerability in workflow dispatch script.
Expanding workflow inputs directly via
${{ inputs.tag }}and${{ inputs.notes }}inside inline shell scripts creates a potential script injection vector if special characters or quotes are included in the inputs. It also causes the step to fail with syntax errors if the notes contain double quotes.Passing these inputs as environment variables securely prevents shell expansion and ensures quoting reliability.
[pass 1]