@yansigit/opencodex 2.31.2 → 2.31.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -94,10 +94,35 @@ function compileContents(value: unknown, toWireName: (name: string) => string):
94
94
  });
95
95
  }
96
96
 
97
+ const GOOGLE_BUILTIN_TOOL_KEYS = new Set([
98
+ "googleSearch", "google_search",
99
+ "urlContext", "url_context",
100
+ "codeExecution", "code_execution",
101
+ ]);
102
+
103
+ function isGoogleBuiltinToolObject(rawTool: unknown): boolean {
104
+ if (!isObject(rawTool)) return false;
105
+ const keys = Object.keys(rawTool);
106
+ return keys.length > 0 && keys.every(key => GOOGLE_BUILTIN_TOOL_KEYS.has(key));
107
+ }
108
+
109
+ function passthroughGoogleBuiltinTools(rawTool: unknown): unknown[] {
110
+ if (!isObject(rawTool) || Array.isArray(rawTool.functionDeclarations)) return [];
111
+ if (!isGoogleBuiltinToolObject(rawTool)) return [];
112
+ const out: Record<string, unknown> = {};
113
+ for (const key of Object.keys(rawTool)) {
114
+ if (key === "googleSearch" || key === "google_search") out.google_search = {};
115
+ else if (key === "urlContext" || key === "url_context") out.url_context = {};
116
+ else if (key === "codeExecution" || key === "code_execution") out.code_execution = rawTool[key] ?? {};
117
+ }
118
+ return Object.keys(out).length > 0 ? [out] : [];
119
+ }
120
+
97
121
  function compileTools(value: unknown, toWireName: (name: string) => string): unknown[] | undefined {
98
122
  if (!Array.isArray(value)) return undefined;
99
123
  const tools = value.flatMap(rawTool => {
100
- if (!isObject(rawTool) || !Array.isArray(rawTool.functionDeclarations)) return [];
124
+ const builtins = passthroughGoogleBuiltinTools(rawTool);
125
+ if (!isObject(rawTool) || !Array.isArray(rawTool.functionDeclarations)) return builtins;
101
126
  const functionDeclarations = rawTool.functionDeclarations.flatMap(rawDeclaration => {
102
127
  if (!isObject(rawDeclaration) || typeof rawDeclaration.name !== "string") return [];
103
128
  return [{
@@ -106,7 +131,10 @@ function compileTools(value: unknown, toWireName: (name: string) => string): unk
106
131
  parameters: sanitizeGeminiToolParameters(rawDeclaration.parameters),
107
132
  }];
108
133
  });
109
- return functionDeclarations.length > 0 ? [{ functionDeclarations }] : [];
134
+ return [
135
+ ...builtins,
136
+ ...(functionDeclarations.length > 0 ? [{ functionDeclarations }] : []),
137
+ ];
110
138
  });
111
139
  return tools.length > 0 ? tools : undefined;
112
140
  }
@@ -238,3 +266,56 @@ export function repairGoogleInvalidRequestBody(body: string, errorPayload: strin
238
266
  }
239
267
  return changed ? JSON.stringify(parsed) : undefined;
240
268
  }
269
+
270
+ function stripBuiltinToolsFromRoot(root: JsonObject): boolean {
271
+ if (!Array.isArray(root.tools)) return false;
272
+ const before = root.tools.length;
273
+ const filtered = root.tools.filter(rawTool => !isGoogleBuiltinToolObject(rawTool));
274
+ root.tools = filtered;
275
+ return filtered.length !== before;
276
+ }
277
+
278
+ /** One mixed-tool 400 replay: drop known built-in siblings and keep functionDeclarations. */
279
+ export function stripGoogleBuiltinToolsFromWireBody(body: string): string | undefined {
280
+ let parsed: unknown;
281
+ try {
282
+ parsed = JSON.parse(body) as unknown;
283
+ } catch {
284
+ return undefined;
285
+ }
286
+ if (!isObject(parsed)) return undefined;
287
+ const root = isObject(parsed.request) ? parsed.request : parsed;
288
+ if (!stripBuiltinToolsFromRoot(root)) return undefined;
289
+ return JSON.stringify(parsed);
290
+ }
291
+
292
+ export function isGoogleMixedBuiltinToolError(errorPayload: string): boolean {
293
+ const mentionsBuiltin = /\b(?:google[_ ]?search|url[_ ]?context|code[_ ]?execution|built[- ]?in(?:\s+tools?)?)\b/i.test(errorPayload);
294
+ if (!mentionsBuiltin) return false;
295
+
296
+ // A mixed-tool error names a builtin AND a function/tool term AND a coexistence verb.
297
+ // Builtin mention is gated above; each pattern below requires a coexistence verb to
298
+ // appear within a bounded window of a tool/function/declaration term (or the phrase
299
+ // itself implies coexistence, e.g. "mutually exclusive"). Bare "coexist"/"alongside"
300
+ // without a nearby tool/function term is NOT enough — a schema error can say fields
301
+ // "coexist" while merely mentioning a builtin.
302
+ const describesIncompatibleCoexistence = [
303
+ /\bmix\w*\b[^\n.!?]{0,80}\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b/i,
304
+ /\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b[^\n.!?]{0,80}\bmix\w*\b/i,
305
+ /\bcombin\w*\b[^\n.!?]{0,80}\bwith\b/i,
306
+ /\bcoexist\w*\b[^\n.!?]{0,80}\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b/i,
307
+ /\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b[^\n.!?]{0,80}\bcoexist\w*\b/i,
308
+ /\balongside\b[^\n.!?]{0,80}\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b/i,
309
+ /\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b[^\n.!?]{0,80}\balongside\b/i,
310
+ /\buse\w*\b[^\n.!?]{0,80}\btogether\b/i,
311
+ /\btogether\b[^\n.!?]{0,80}\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b/i,
312
+ /\buse\w*\b[^\n.!?]{0,80}\bwith\b[^\n.!?]{0,80}\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b/i,
313
+ /\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b[^\n.!?]{0,80}\bwith\b[^\n.!?]{0,80}\buse\w*\b/i,
314
+ /\bnot supported with\b[^\n.!?]{0,80}\b(?:tool\w*|function(?:[_ ]declarations?)?|declaration\w*)\b/i,
315
+ /\bmutually exclusive\b/i,
316
+ /\bincompatib\w*\b[^\n.!?]{0,80}\b(?:coexist\w*|combin\w*|alongside|used together|mutually exclusive)\b/i,
317
+ /\b(?:coexist\w*|combin\w*|alongside|used together|mutually exclusive)\b[^\n.!?]{0,80}\bincompatib\w*\b/i,
318
+ ].some(pattern => pattern.test(errorPayload));
319
+
320
+ return describesIncompatibleCoexistence;
321
+ }
@@ -28,6 +28,12 @@ import { sanitizeGeminiToolParameters } from "./google-tool-schema";
28
28
  import { identifyRoutedModel } from "./identity";
29
29
  import { antigravityUsesReplayCache, applyAntigravityReplay, clearAntigravityReplay, observeAntigravityReplay } from "./google-antigravity-replay";
30
30
  import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
31
+ import {
32
+ extractCcaGroundingSources,
33
+ formatCcaGroundingSourcesAppendix,
34
+ isCcaSearchSuggestionHtml,
35
+ } from "../web-search/gemini-executor";
36
+ import type { WebSearchSource } from "../web-search/parse";
31
37
  import { googleVertexLocationConfigError } from "../providers/google-vertex-location";
32
38
  import { lookupReplayThoughtSignature } from "../responses/thought-signature-replay";
33
39
  import {
@@ -335,22 +341,29 @@ function messagesToGeminiFormat(
335
341
  return { systemInstruction, contents };
336
342
  }
337
343
 
338
- function toolsToGeminiFormat(parsed: OcxParsedRequest): unknown[] | undefined {
339
- if (!parsed.context.tools?.length) return undefined;
344
+ function toolsToGeminiFormat(
345
+ parsed: OcxParsedRequest,
346
+ wireModelId: string,
347
+ ): unknown[] | undefined {
348
+ const grounding = parsed._ccaInTurnGrounding;
340
349
  const allowed = isAllowedToolChoice(parsed.options.toolChoice)
341
350
  ? new Set(parsed.options.toolChoice.allowedTools)
342
351
  : undefined;
343
352
  const tools = allowed
344
- ? parsed.context.tools.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools))
345
- : parsed.context.tools;
346
- if (tools.length === 0) return undefined;
347
- return [{
348
- functionDeclarations: tools.map(t => ({
349
- name: namespacedToolName(t.namespace, t.name),
350
- description: t.description,
351
- parameters: t.parameters,
352
- })),
353
- }];
353
+ ? parsed.context.tools?.filter(t => toolAllowedByChoice(t, allowed, parsed.context.tools)) ?? []
354
+ : parsed.context.tools ?? [];
355
+ const functionDeclarations = tools.map(t => ({
356
+ name: namespacedToolName(t.namespace, t.name),
357
+ description: t.description,
358
+ parameters: t.parameters,
359
+ }));
360
+ const wireTools: unknown[] = [];
361
+ if (functionDeclarations.length > 0) wireTools.push({ functionDeclarations });
362
+ if (grounding && !/claude/i.test(wireModelId)) {
363
+ if (grounding.search) wireTools.push({ google_search: {} });
364
+ if (grounding.urlContext) wireTools.push({ url_context: {} });
365
+ }
366
+ return wireTools.length > 0 ? wireTools : undefined;
354
367
  }
355
368
 
356
369
  /**
@@ -509,11 +522,12 @@ function googleToolCallMetadataFromPart(
509
522
  * Keep that provider visibility bit authoritative here so the streaming and buffered parsers
510
523
  * cannot accidentally expose the same hidden reasoning through different event types.
511
524
  */
512
- function googlePartTextEvent(part: GoogleResponsePart): AdapterEvent | undefined {
525
+ function googlePartTextEvent(part: GoogleResponsePart, filterCcaSearchSuggestionHtml = false): AdapterEvent | undefined {
513
526
  // A malformed scalar/object is not text and must not cross the AdapterEvent boundary. Dropping
514
527
  // only this optional field preserves the rest of the part without inventing assistant output by
515
528
  // coercion; an empty string keeps its existing no-event behavior.
516
529
  if (typeof part.text !== "string" || part.text.length === 0) return undefined;
530
+ if (filterCcaSearchSuggestionHtml && isCcaSearchSuggestionHtml(part.text)) return undefined;
517
531
  return part.thought === true
518
532
  ? { type: "reasoning_raw_delta", text: part.text }
519
533
  : { type: "text_delta", text: part.text };
@@ -669,6 +683,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
669
683
  let vertexReplayModel: string | undefined;
670
684
  let vertexReplaySession: string | undefined;
671
685
  let restoreGoogleToolName = (name: string): string => name;
686
+ const emitInTurnGroundingSourcesQueue: boolean[] = [];
672
687
  return {
673
688
  name: "google",
674
689
 
@@ -702,7 +717,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
702
717
  identityModelId,
703
718
  provider.googleMode === "cloud-code-assist",
704
719
  );
705
- const tools = toolsToGeminiFormat(parsed);
720
+ const tools = toolsToGeminiFormat(parsed, routedModelId);
706
721
 
707
722
  const body: Record<string, unknown> = { contents };
708
723
  if (systemInstruction) body.systemInstruction = systemInstruction;
@@ -836,6 +851,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
836
851
  if (/claude/i.test(wireModelId)) {
837
852
  headers["anthropic-beta"] = "interleaved-thinking-2025-05-14";
838
853
  }
854
+ emitInTurnGroundingSourcesQueue.push(!!parsed._ccaInTurnGrounding);
839
855
  return { url, method: "POST", headers, body: JSON.stringify(envelope) };
840
856
  }
841
857
 
@@ -861,6 +877,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
861
877
  if (apiKey) {
862
878
  const url = `https://aiplatform.googleapis.com/v1/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
863
879
  headers["x-goog-api-key"] = apiKey;
880
+ emitInTurnGroundingSourcesQueue.push(!!parsed._ccaInTurnGrounding);
864
881
  return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
865
882
  }
866
883
  const project = provider.project || process.env.GOOGLE_CLOUD_PROJECT || process.env.GCLOUD_PROJECT;
@@ -873,6 +890,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
873
890
  const url = `https://${host}/v1/projects/${project}/locations/${location}/publishers/google/models/${parsed.modelId}:${method}${streamParam}`;
874
891
  const token = await getVertexAccessToken();
875
892
  headers["Authorization"] = `Bearer ${token}`;
893
+ emitInTurnGroundingSourcesQueue.push(!!parsed._ccaInTurnGrounding);
876
894
  return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
877
895
  }
878
896
 
@@ -884,10 +902,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
884
902
 
885
903
  const compiled = compileGoogleWireBody(body);
886
904
  restoreGoogleToolName = compiled.restoreToolName;
905
+ emitInTurnGroundingSourcesQueue.push(!!parsed._ccaInTurnGrounding);
887
906
  return { url, method: "POST", headers, body: JSON.stringify(compiled.body) };
888
907
  },
889
908
 
890
909
  async *parseStream(response: Response, budget: TranslatorBudget): AsyncGenerator<AdapterEvent> {
910
+ const emitInTurnGroundingSources = emitInTurnGroundingSourcesQueue.shift() ?? false;
911
+ const filterCcaSearchSuggestionHtml =
912
+ provider.googleMode === "cloud-code-assist" && emitInTurnGroundingSources;
891
913
  if (!response.body) {
892
914
  yield { type: "error", message: "No response body" };
893
915
  return;
@@ -912,6 +934,14 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
912
934
  let sawAnyFrame = false;
913
935
  let sawTerminalSignal = false;
914
936
  let pendingStreamThoughtSig: string | undefined;
937
+ const groundingSources: WebSearchSource[] = [];
938
+ let groundingSourcesEmitted = false;
939
+
940
+ const mergeGroundingSources = (groundingMetadata: unknown): void => {
941
+ for (const source of extractCcaGroundingSources(groundingMetadata)) {
942
+ if (!groundingSources.some(existing => existing.url === source.url)) groundingSources.push(source);
943
+ }
944
+ };
915
945
 
916
946
  const handleDataLine = async function* (line: string): AsyncGenerator<AdapterEvent, "continue" | "content" | "terminate"> {
917
947
  const payload = line.slice(5).trim();
@@ -1006,8 +1036,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1006
1036
  const candidate = rawCandidate as {
1007
1037
  content?: unknown;
1008
1038
  finishReason?: string;
1039
+ groundingMetadata?: unknown;
1009
1040
  };
1010
1041
 
1042
+ if (emitInTurnGroundingSources && candidate.groundingMetadata) {
1043
+ mergeGroundingSources(candidate.groundingMetadata);
1044
+ }
1045
+
1011
1046
  if (typeof candidate.finishReason === "string" && candidate.finishReason) {
1012
1047
  lastFinishReason = candidate.finishReason;
1013
1048
  sawTerminalSignal = true;
@@ -1055,7 +1090,7 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1055
1090
  if (part.thought === true && sig && isLikelyRealThoughtSignature(sig)) {
1056
1091
  pendingStreamThoughtSig = sig;
1057
1092
  }
1058
- const textEvent = googlePartTextEvent(part);
1093
+ const textEvent = googlePartTextEvent(part, filterCcaSearchSuggestionHtml);
1059
1094
  if (textEvent) {
1060
1095
  emittedContentEvent = true;
1061
1096
  yield textEvent;
@@ -1166,6 +1201,13 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
1166
1201
  yield { type: "error", message: "upstream stream ended without a terminal signal — possible truncation" };
1167
1202
  return;
1168
1203
  }
1204
+ if (emitInTurnGroundingSources && !groundingSourcesEmitted && groundingSources.length > 0) {
1205
+ const appendix = formatCcaGroundingSourcesAppendix(groundingSources);
1206
+ if (appendix) {
1207
+ groundingSourcesEmitted = true;
1208
+ yield { type: "text_delta", text: appendix };
1209
+ }
1210
+ }
1169
1211
  const stopReason = lastFinishReason === "MAX_TOKENS"
1170
1212
  ? "max_tokens"
1171
1213
  : ["SAFETY", "RECITATION", "BLOCKLIST", "PROHIBITED_CONTENT", "SPII"].includes(lastFinishReason ?? "")
package/src/config.ts CHANGED
@@ -735,6 +735,8 @@ const providerConfigSchema = z.object({
735
735
  // accepted, persisted, and then silently resolved to the `code_mode_only` default — the
736
736
  // operator asked for shell mode, got code mode, and was told nothing (#2106).
737
737
  codexToolMode: z.enum(["code_mode_only", "shell"]).optional(),
738
+ // Validated rather than passed through: same rationale as codexToolMode above.
739
+ projectContext: z.enum(["off", "on"]).optional(),
738
740
  responsesItemIdRepair: z.object({
739
741
  message: z.array(z.string().min(1)).optional(),
740
742
  reasoning: z.array(z.string().min(1)).optional(),
@@ -10,7 +10,7 @@
10
10
  },
11
11
  {
12
12
  "path": "package.json",
13
- "sha256": "5f79caaeac059bcbe36457e17d5c74ebaf9a0ec4779e74f1099db586395cbcb8"
13
+ "sha256": "fa2d431959a8bfdaa38e582dc186e5cea86bd59a191cb47e590c4b769d31c497"
14
14
  },
15
15
  {
16
16
  "path": "scripts/model-metadata.source.json",
@@ -52,9 +52,13 @@
52
52
  "path": "src/adapters/cline-pass-deepseek-v4-tool-replay.ts",
53
53
  "sha256": "da664327db4e0bae8ab689230deec415ad3f6d0f18c596dcc5b94c8a8d08a345"
54
54
  },
55
+ {
56
+ "path": "src/adapters/command-code-project-context.ts",
57
+ "sha256": "72bcc19ad2ce2a3f743391d6388c67912e3cf3bfc244dd6b410638c2c34241a5"
58
+ },
55
59
  {
56
60
  "path": "src/adapters/command-code.ts",
57
- "sha256": "de5e96d0c8d15ae11c7db6332b73387fe56d12ea38b87e8f2a9d8b805c3dc94d"
61
+ "sha256": "dbed5da61ccf72aa3a250270e8b25ada850c05fce06b10326b2fc132416aa24a"
58
62
  },
59
63
  {
60
64
  "path": "src/adapters/cursor.ts",
@@ -122,7 +126,7 @@
122
126
  },
123
127
  {
124
128
  "path": "src/adapters/cursor/live-transport.ts",
125
- "sha256": "cc994819e61adca9fdf7ed22a84c726e02b61db93abdd7dd4eb21051f633c08e"
129
+ "sha256": "797e54c0aa7cd032aba8947016178d7346dded57506eb767ce635523f8ed5d71"
126
130
  },
127
131
  {
128
132
  "path": "src/adapters/cursor/mcp-config.ts",
@@ -136,6 +140,10 @@
136
140
  "path": "src/adapters/cursor/message-mapper.ts",
137
141
  "sha256": "aa83d8db1616a8f43badb15dfc6ee8601e0604a50cb91569555f0f4488418f1c"
138
142
  },
143
+ {
144
+ "path": "src/adapters/cursor/native-exec-bridge.ts",
145
+ "sha256": "0a5c5f67d0ab02563f57e3f0dfdac096c2586b9e46b8f51601063d6e531e5a40"
146
+ },
139
147
  {
140
148
  "path": "src/adapters/cursor/native-exec-common.ts",
141
149
  "sha256": "4682a899e9e626fc7fe7a481bf565468c44641d30414d2e3de560c703ce090b8"
@@ -226,7 +234,7 @@
226
234
  },
227
235
  {
228
236
  "path": "src/adapters/google-http.ts",
229
- "sha256": "7777e3c6af84a53cf9fdb6caaf85f67e51160e9745f154548b0f21da44f990c0"
237
+ "sha256": "1432cd6bc49988200e87d8703f0659928efa937806e76f6522167ea62b360d56"
230
238
  },
231
239
  {
232
240
  "path": "src/adapters/google-tool-schema.ts",
@@ -238,11 +246,11 @@
238
246
  },
239
247
  {
240
248
  "path": "src/adapters/google-wire-compiler.ts",
241
- "sha256": "5f3b21dc2fd6cbd584a83e2537d5a7012b179f76b575afd83ff17a3acdb7ad8d"
249
+ "sha256": "b6fc399cc8945e43ceaeb9580a69a28c10f6941e6c567802485f5589437f2086"
242
250
  },
243
251
  {
244
252
  "path": "src/adapters/google.ts",
245
- "sha256": "74ad714b7c4ff969f9e80af8f9d6cbab6a3727b18394a07775be30fa0ff9bb56"
253
+ "sha256": "eae2446639701e12df75bade1fd871d6275efda162fe42ba09353c42f9b5f197"
246
254
  },
247
255
  {
248
256
  "path": "src/adapters/identity.ts",
@@ -1098,7 +1106,7 @@
1098
1106
  },
1099
1107
  {
1100
1108
  "path": "src/config.ts",
1101
- "sha256": "82609b9c2f6ae4bb7cc918e6737362daa23f2361909858a11b7e344d9d4d1097"
1109
+ "sha256": "d87f6e979e9e4e51ac958ca2ab8e3e48401894f5cfa166924430ce45089528cc"
1102
1110
  },
1103
1111
  {
1104
1112
  "path": "src/config/provider-name.ts",
@@ -1694,7 +1702,7 @@
1694
1702
  },
1695
1703
  {
1696
1704
  "path": "src/lab/subject/behavior-fingerprint.ts",
1697
- "sha256": "9dc54211f2550f404dbe85505938c9b4c559548d3fa733db82860ddb50ea2d9f"
1705
+ "sha256": "bbc988927141dc5e4d4f895b3ffbdded4a07153802ea62b1ad775d136f7a6022"
1698
1706
  },
1699
1707
  {
1700
1708
  "path": "src/lab/subject/installation-salt.ts",
@@ -2046,7 +2054,7 @@
2046
2054
  },
2047
2055
  {
2048
2056
  "path": "src/oauth/index.ts",
2049
- "sha256": "4b6603e57e1b5b13a37c27876ba1d85066e145b06507d40923ede0f6c0892625"
2057
+ "sha256": "d867f09896669ab76d4886daa7ddd9b91476f3fbcbc6b9f41c518e3aab04e393"
2050
2058
  },
2051
2059
  {
2052
2060
  "path": "src/oauth/key-providers.ts",
@@ -2386,7 +2394,7 @@
2386
2394
  },
2387
2395
  {
2388
2396
  "path": "src/routing/compatibility/behavior.ts",
2389
- "sha256": "99a85b286aa66b5f46a2f95b9da32294a1bb3a9e8c00737d0aafa45063b37476"
2397
+ "sha256": "1c3008ab085dc8bb0033f23b5eaa640553484036db1ad3e44ccbff1d12618225"
2390
2398
  },
2391
2399
  {
2392
2400
  "path": "src/routing/compatibility/catalog.ts",
@@ -2774,7 +2782,7 @@
2774
2782
  },
2775
2783
  {
2776
2784
  "path": "src/server/responses/core.ts",
2777
- "sha256": "889f74b70fb60c2c7bff2559fc923b64a883a5444aa4757ef1eb2c4a450e8e2d"
2785
+ "sha256": "e0f29663b051f0ba820b1d0a5ca460a6dcde3f219be950db91e53f305d87a07d"
2778
2786
  },
2779
2787
  {
2780
2788
  "path": "src/server/responses/empty-completion-guard.ts",
@@ -2958,11 +2966,11 @@
2958
2966
  },
2959
2967
  {
2960
2968
  "path": "src/types/provider.ts",
2961
- "sha256": "6a37ade0ad24851f61705584e0c67f1c64335541ffcd5f337465ddd618249f39"
2969
+ "sha256": "e1b496ef630fc6bbd2829eda217975a1aaa464ff27c9a2a5dabba44220c82dfb"
2962
2970
  },
2963
2971
  {
2964
2972
  "path": "src/types/request.ts",
2965
- "sha256": "4a5e832cbd05b3c9cdeba818d2cdeeb10cb5da93349140e0c093fd99d66799d0"
2973
+ "sha256": "729aa89dcc03f4255a2e5fdb8f5000efc73625072a855d3e52eec543841bbc20"
2966
2974
  },
2967
2975
  {
2968
2976
  "path": "src/types/tools.ts",
@@ -3106,11 +3114,11 @@
3106
3114
  },
3107
3115
  {
3108
3116
  "path": "src/web-search/gemini-executor.ts",
3109
- "sha256": "51fc8703f43b34e0a437748d31577e30fa30e408b356129ee1680c0b59d7a0b1"
3117
+ "sha256": "7a2d576680af12d9894896dac3bfaab9cec62a3f959857271bc041baba52f83e"
3110
3118
  },
3111
3119
  {
3112
3120
  "path": "src/web-search/index.ts",
3113
- "sha256": "f9de3df23c1184f0a7b09ae6ebe9857824dd23a79132bd11d3e36e9a5b6268e4"
3121
+ "sha256": "6751c704b888ac114cca3b7f2e6dc840ceb33164b2d585e74aa3a3c399bee1fb"
3114
3122
  },
3115
3123
  {
3116
3124
  "path": "src/web-search/loop.ts",
@@ -3,7 +3,7 @@ import { jcsStringify } from "../digest";
3
3
  import type { LabBehaviorSource, LabBehaviorValues } from "../live/types";
4
4
 
5
5
  const CLOSED_KEYS = new Set([
6
- "wire.adapter", "wire.upstreamProtocol", "wire.responsesPath", "wire.commandCodeVersion", "wire.modelSuffixMode",
6
+ "wire.adapter", "wire.upstreamProtocol", "wire.responsesPath", "wire.commandCodeVersion", "wire.commandCodeProjectContext", "wire.modelSuffixMode",
7
7
  "auth.mode", "auth.transport",
8
8
  "responses.stateful", "responses.upstreamStreaming", "responses.serviceTier", "responses.fastWireKind", "responses.fastWireValue", "responses.snapshotRepair", "responses.itemIdRepair",
9
9
  "limits.contextWindow", "limits.maxInputTokens", "limits.maxOutputTokens",
@@ -1073,6 +1073,9 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
1073
1073
  if (existing?.commandCodeVersion !== undefined) {
1074
1074
  next.commandCodeVersion = existing.commandCodeVersion;
1075
1075
  }
1076
+ if (existing?.projectContext !== undefined) {
1077
+ next.projectContext = existing.projectContext;
1078
+ }
1076
1079
  // User-configured price overlays are operator data, not preset state; a
1077
1080
  // re-login, add-account, or reauth must not silently drop them from the
1078
1081
  // Logs/Usage estimates.
@@ -148,6 +148,9 @@ export function resolveProductionBehaviorValues(
148
148
  "wire.upstreamProtocol": behaviorRow("provider_config", upstreamProtocol),
149
149
  "wire.responsesPath": behaviorRow("provider_config", effective.responsesPath ?? null),
150
150
  "wire.commandCodeVersion": behaviorRow("provider_config", effective.commandCodeVersion ?? null),
151
+ ...(adapter === "command-code"
152
+ ? { "wire.commandCodeProjectContext": behaviorRow("provider_config", effective.projectContext ?? "off") }
153
+ : {}),
151
154
  "wire.modelSuffixMode": behaviorRow(
152
155
  "provider_config",
153
156
  effective.modelSuffixBracketStrip === true ? "bracket_strip" : "none",
@@ -125,7 +125,7 @@ import {
125
125
  rotateCursorAccountOnAuth,
126
126
  } from "../../oauth/cursor-routing";
127
127
  import { getAccountCredential } from "../../oauth/store";
128
- import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
128
+ import { buildWebSearchTool, mediaBridgeWillRun, planWebSearch, resolveCcaInTurnGrounding, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
129
129
  import { buildImageTool, buildVideoTool, planImageBridge, planVideoBridge, runWithImageBridge, clampImageMaxRounds, IMAGE_GEN_TOOL_NAME, VIDEO_GEN_TOOL_NAME } from "../../images";
130
130
  import { describeImagesInPlace, isModelTextOnly, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
131
131
  import { createAdapterEventQueue, preflightAdapterEvents, type AdapterEventQueue } from "../../adapters/run-turn-queue";
@@ -4061,13 +4061,23 @@ async function handleResponsesInner(
4061
4061
  // - non-runTurn: web-search wins over image when both eligible (documented priority)
4062
4062
  // - runTurn: image bridge may run (it supports runTurn); web-search is skipped so runTurn
4063
4063
  // can proceed for web-search-only turns
4064
- const wsPlan = !routedCompaction
4065
- ? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar)
4066
- : undefined;
4067
4064
  const imgPlan = !routedCompaction ? await planImageBridge(config, parsed, route.provider) : undefined;
4068
4065
  const vidPlan = !routedCompaction ? await planVideoBridge(config, parsed, route.provider) : undefined;
4069
- const canRunWebSearch = !!wsPlan && !adapter.runTurn;
4070
- if ((imgPlan || vidPlan) && canRunWebSearch) {
4066
+ const hasMediaPlan = !!(imgPlan || vidPlan);
4067
+ // A media plan is not enough to suppress Gemini 2.x grounding: both bridges inject tools only
4068
+ // on streaming turns. This is the potential-injection value used to resolve sidecar precedence.
4069
+ const mediaMayInject = mediaBridgeWillRun(hasMediaPlan, false, !!adapter.runTurn, parsed.stream);
4070
+ const wsPlan = !routedCompaction
4071
+ ? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar, mediaMayInject)
4072
+ : undefined;
4073
+ const webSearchWinsMedia = !!wsPlan && !adapter.runTurn;
4074
+ const mediaWillRun = mediaBridgeWillRun(hasMediaPlan, !!wsPlan, !!adapter.runTurn, parsed.stream);
4075
+ const ccaInTurnGrounding = !routedCompaction
4076
+ ? resolveCcaInTurnGrounding(config, parsed, false, route.provider, route.modelId, mediaWillRun)
4077
+ : undefined;
4078
+ if (ccaInTurnGrounding) parsed._ccaInTurnGrounding = ccaInTurnGrounding;
4079
+ const canRunWebSearch = webSearchWinsMedia && !ccaInTurnGrounding;
4080
+ if (hasMediaPlan && webSearchWinsMedia) {
4071
4081
  // Web search takes priority when both are active — the media bridge cannot run
4072
4082
  // alongside runWithWebSearch. Surface a runtime signal so the user knows their
4073
4083
  // configured video/image bridge was skipped for this turn, rather than silently
@@ -4075,7 +4085,7 @@ async function handleResponsesInner(
4075
4085
  if (vidPlan) console.warn("[videos] video bridge skipped: web search is active for this turn");
4076
4086
  if (imgPlan) console.warn("[images] image bridge skipped: web search is active for this turn");
4077
4087
  }
4078
- if ((imgPlan || vidPlan) && (!wsPlan || adapter.runTurn)) {
4088
+ if (mediaWillRun || (imgPlan && !parsed.stream && !webSearchWinsMedia)) {
4079
4089
  // The image bridge detects a hosted image_generation tool and requires streaming.
4080
4090
  // The video bridge activates from config and injects a tool — it also needs streaming
4081
4091
  // (the loop returns SSE). For video-only (no imgPlan) on a non-streaming request, skip
@@ -169,6 +169,13 @@ export interface OcxProviderConfig {
169
169
  * version here instead of waiting for a code change. Absent uses the adapter's current default.
170
170
  */
171
171
  commandCodeVersion?: string;
172
+ /**
173
+ * Command Code OAuth `/alpha/generate` project-context envelope. When `"on"`, the adapter
174
+ * fills `memory`, `taste`, and `skills` from bounded files under `process.cwd()`. Absent or
175
+ * `"off"` keeps the empty envelope (`memory: ""`, `taste: null`, `skills: null`). Does not
176
+ * enable taste learning (`x-taste-learning` stays `"false"`).
177
+ */
178
+ projectContext?: "off" | "on";
172
179
  /**
173
180
  * Responses upstream that stores nothing server-side (DeepSeek documents "the API
174
181
  * is stateless"). Stateful request parameters are dropped, `store` is pinned false,
@@ -94,6 +94,12 @@ export interface OcxParsedRequest {
94
94
  * executes searches via the gpt-5.4-mini sidecar (see src/web-search). Absent when not requested.
95
95
  */
96
96
  _webSearch?: Record<string, unknown>;
97
+ /**
98
+ * Antigravity Gemini in-turn CCA grounding: google_search and optional url_context ride the main
99
+ * routed fetch instead of the web-search sidecar loop. Set by core.ts when resolveCcaInTurnGrounding
100
+ * matches; consumed by the Google adapter at buildRequest/parseStream time.
101
+ */
102
+ _ccaInTurnGrounding?: { search: boolean; urlContext: boolean };
97
103
  /** Hosted image_generation tool config stashed for the image bridge sidecar (see src/images). */
98
104
  _imageGeneration?: { toolNames: Set<string>; originalTool?: Record<string, unknown> };
99
105
  /**
@@ -19,6 +19,7 @@ import { ANTIGRAVITY_REQUEST_UA } from "../adapters/google-antigravity-wire";
19
19
  import { resolveAntigravityEffortWireModel } from "../providers/antigravity-models";
20
20
  import { getProviderRegistryEntry } from "../providers/registry";
21
21
  import { MAX_SIDECAR_RESPONSE_BYTES, type WebSearchSource } from "./parse";
22
+ import { appendSafeWebSearchSource } from "./sources";
22
23
  import { BASE_INSTRUCTION, IMAGE_INSTRUCTION, type SidecarOutcome, type SidecarSettings } from "./executor";
23
24
 
24
25
  const CCA_FALLBACK_BASE = "https://daily-cloudcode-pa.googleapis.com";
@@ -117,25 +118,46 @@ export async function runGeminiWebSearch(
117
118
  }
118
119
 
119
120
  /** Map a CCA generateContent payload (possibly wrapped in {response}) to text + grounding sources. */
121
+ export function extractCcaGroundingSources(groundingMetadata: unknown): WebSearchSource[] {
122
+ const sources: WebSearchSource[] = [];
123
+ const gm = isRec(groundingMetadata) ? groundingMetadata : undefined;
124
+ if (!gm || !Array.isArray(gm.groundingChunks)) return sources;
125
+ for (const chunk of gm.groundingChunks) {
126
+ const web = isRec(chunk) && isRec(chunk.web) ? chunk.web : undefined;
127
+ if (!web) continue;
128
+ // Route through the shared safe-source validator: enforces http(s) scheme,
129
+ // control-character filtering, dedup, and per-message count/byte budgets.
130
+ appendSafeWebSearchSource(sources, { url: web.uri, title: web.title });
131
+ }
132
+ return sources;
133
+ }
134
+
135
+ /** Skip search-suggestion HTML widgets Google may attach alongside grounded answers. */
136
+ export function isCcaSearchSuggestionHtml(text: string): boolean {
137
+ const trimmed = text.trim();
138
+ if (!trimmed.startsWith("<")) return false;
139
+ return /<style[\s>]/i.test(trimmed) || /search[_-]?suggest/i.test(trimmed) || /grounding-widget/i.test(trimmed);
140
+ }
141
+
142
+ export function formatCcaGroundingSourcesAppendix(sources: readonly WebSearchSource[]): string {
143
+ if (sources.length === 0) return "";
144
+ const lines = ["", "Sources:"];
145
+ sources.slice(0, 8).forEach((s, i) => {
146
+ lines.push(`[${i + 1}] ${s.title ? `${s.title} — ` : ""}${s.url}`);
147
+ });
148
+ return lines.join("\n");
149
+ }
150
+
120
151
  export function mapCcaGroundedResponse(payload: unknown): SidecarOutcome {
121
152
  const root = isRec(payload) && isRec(payload.response) ? payload.response : payload;
122
153
  if (!isRec(root)) return { text: "", sources: [], error: "gemini sidecar returned a non-JSON or empty body" };
123
154
  const candidate = Array.isArray(root.candidates) && isRec(root.candidates[0]) ? root.candidates[0] : undefined;
124
155
  if (!candidate) return { text: "", sources: [], error: "gemini sidecar returned no candidates" };
125
156
  const parts = isRec(candidate.content) && Array.isArray(candidate.content.parts) ? candidate.content.parts : [];
126
- const text = parts.map(p => (isRec(p) && typeof p.text === "string" ? p.text : "")).join("");
127
- const sources: WebSearchSource[] = [];
128
- const seen = new Set<string>();
129
- const gm = isRec(candidate.groundingMetadata) ? candidate.groundingMetadata : undefined;
130
- if (gm && Array.isArray(gm.groundingChunks)) {
131
- for (const chunk of gm.groundingChunks) {
132
- const web = isRec(chunk) && isRec(chunk.web) ? chunk.web : undefined;
133
- const uri = web && typeof web.uri === "string" ? web.uri : undefined;
134
- if (!uri || seen.has(uri)) continue;
135
- seen.add(uri);
136
- sources.push({ url: uri, ...(typeof web?.title === "string" && web.title.length > 0 ? { title: web.title } : {}) });
137
- }
138
- }
157
+ const text = parts
158
+ .map(p => (isRec(p) && typeof p.text === "string" && !isCcaSearchSuggestionHtml(p.text) ? p.text : ""))
159
+ .join("");
160
+ const sources = extractCcaGroundingSources(candidate.groundingMetadata);
139
161
  if (text.length === 0) return { text: "", sources, error: "gemini sidecar returned no text" };
140
162
  return { text, sources };
141
163
  }