Skip to content

Commit c9f2c84

Browse files
author
pavelhov
authored
Merge pull request #67 from pavelhov/codex/xai-x-search-0.1.15
feat: add Grok 4.7 X search through xAI
2 parents 23cfdcf + 9e74ce8 commit c9f2c84

14 files changed

Lines changed: 961 additions & 35 deletions

File tree

‎docs-site/src/content/docs/guides/sidecars.md‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,40 @@ request. The effective bridge watchdog is
6767
stall is not a total generation timeout. Failures before SSE starts return non-2xx JSON; generation
6868
failures after response headers have started are delivered as `response.failed` SSE.
6969

70+
### X search for Grok 4.7
71+
72+
When a turn routes to `xai/grok-4.7` on the xAI provider, CodexCommander also gives the model a
73+
synthetic `x_search` function tool. The model calls it only when a task needs live X (Twitter)
74+
research, such as what people are posting about a topic or what a specific account said. Other
75+
providers and other Grok models are unchanged, and the turn itself keeps using the existing xAI chat
76+
route, so Codex tools such as `apply_patch` and shell keep working.
77+
78+
Each `x_search` call runs one request to xAI `/v1/responses` with xAI's server-side
79+
[`x_search` tool](https://docs.x.ai/developers/tools/x-search). It uses the same xAI credential and
80+
transport as the routed turn, either your Grok login or an xAI API key. The model can narrow a
81+
search with `allowed_x_handles` or `excluded_x_handles` (up to 20 handles, not both) and
82+
`from_date` / `to_date` (`YYYY-MM-DD`).
83+
84+
The answer returns to the model as an untrusted tool result with direct `x.com` post links. Codex
85+
shows the call as a web-search cell whose sources are those post links. X search counts against the
86+
same `maxSearchesPerTurn` budget and forced-answer rule as web search. Its per-search deadline is
87+
`max(webSearchSidecar.timeoutMs, 90000)` milliseconds.
88+
89+
xAI bills X search on top of tokens: $5 per 1,000 posts and $10 per 1,000 profiles fetched (xAI
90+
pricing as of 2026-09-21; check the xAI page above for current rates). The search query text is sent
91+
to xAI. To turn X search off, set:
92+
93+
```json
94+
{
95+
"webSearchSidecar": {
96+
"xSearch": false
97+
}
98+
}
99+
```
100+
101+
If your client already defines its own `x_search` function, CodexCommander leaves it alone and does
102+
not add the synthetic tool.
103+
70104
## Vision sidecar
71105

72106
When the routed model is listed in its provider's `noVisionModels` and a request carries an image,

‎docs-site/src/content/docs/reference/configuration/server.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ provider-selection behavior.
170170
| `maxSearchesPerTurn?` | `number` | `3` | Real searches allowed per main-model turn. |
171171
| `routedModelStallTimeoutMs?` | `number` | `200000` | Config-file-only routed-model raw-body inactivity deadline. Integer 1–2147483647; every non-empty chunk resets it. |
172172
| `timeoutMs?` | `number` | `60000` | Deadline for one hosted search. |
173+
| `xSearch?` | `boolean` | `true` | Adds the xAI `x_search` tool for Grok 4.7 turns on the xAI provider. `false` turns it off. See [X search for Grok 4.7](/guides/sidecars/#x-search-for-grok-47). |
173174

174175
The OpenAI backend requires a ChatGPT login and enabled ChatGPT `forward` provider. Claude-inbound
175176
routed replays inject main ChatGPT auth into the internal request. The Anthropic backend uses the
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
---
2+
artifact_contract: ce-unified-plan/v1
3+
artifact_readiness: implementation-ready
4+
execution: code
5+
created: 2026-09-25
6+
---
7+
# Grok X Search and macOS update
8+
9+
## Goal Capsule
10+
Enable a Grok 4.7 Codex task to search live X posts through xAI's Responses API, retain direct source links, and ship the change through development, main, and the next authenticated macOS update.
11+
12+
## Requirements
13+
- R1: Requests routed to xAI Grok 4.7 can opt into server-side `x_search` when the task asks for X research.
14+
- R2: Normal function tools, streaming, cancellation, errors, and citations remain usable.
15+
- R3: General web search for other providers remains unchanged.
16+
- R4: User-facing documentation explains when X search is available and its xAI billing.
17+
- R5: Release assets use the existing signed appcast, an incremented immutable build, and verified prior inventory.
18+
19+
## Key Technical Decisions
20+
- KTD-1: Use xAI's `/v1/responses` endpoint for X research. Its documented `x_search` tool is not available on `/chat/completions`; avoid implying model selection alone enables it. Governs R1-R2.
21+
- KTD-2: Preserve Codex's client tool calls and existing provider routes while adding the hosted xAI tool only for eligible xAI requests. Governs R1-R3.
22+
- KTD-3: Complete feature PR to `development`, then promote via PR to `main` before the release. Governs R5.
23+
24+
## Implementation Units
25+
### U1: xAI hosted search path
26+
Inspect `src/providers/registry.ts`, `src/adapters/openai-chat.ts`, `src/adapters/openai-responses.ts`, `src/responses/parser.ts`, `src/bridge.ts`, and `src/web-search/`. Add the smallest sound xAI Responses routing and `x_search` handling. Add focused regression tests in `tests/` for tool injection, X source links, non-X routes, errors, and streaming/cancellation. Depends on no other unit. Covers R1-R3.
27+
28+
### U2: User documentation
29+
Update the relevant `docs-site/` provider/search documentation and translations so they accurately describe availability, cost, and setup. Depends on U1. Covers R4.
30+
31+
### U3: Integration and update
32+
Run `bun run typecheck`, focused tests, `bun run test:parallel`, `bun run privacy:scan`, docs build, and applicable app checks. Review security implications of sending task content to xAI and preserving citations. Merge the feature to `development`, promote to `main` after CI, package the signed macOS update, generate and verify its appcast, then publish and verify release assets. Depends on U1-U2. Covers R5.
33+
34+
## Verification Contract
35+
The existing non-xAI adapter behavior is preserved. Focused tests prove that a Grok X research request carries `x_search` to xAI Responses and returns verifiable post URLs. Full repository checks and PR CI pass. The release asset is signed, feed verification passes, and the published fixed appcast URL serves the new version.
36+
37+
## Definition of Done
38+
The feature is on both `development` and `main`; a new macOS release is published with an authenticated update; the user can update and test X search from the app.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codexcommander",
3-
"version": "0.1.14",
3+
"version": "0.1.15",
44
"private": true,
55
"description": "CodexCommander — universal provider proxy for OpenAI Codex & Claude Code — use any LLM with Codex CLI/App/SDK and Claude Code",
66
"type": "module",

‎src/config.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -997,6 +997,7 @@ const webSearchSidecarSchema = z.object({
997997
reasoning: z.string().optional(),
998998
maxSearchesPerTurn: z.number().int().nonnegative().optional(),
999999
timeoutMs: z.number().int().positive().optional(),
1000+
xSearch: z.boolean().optional(),
10001001
routedModelStallTimeoutMs: z.number().int().min(1).max(2_147_483_647).optional(),
10011002
}).strict();
10021003

‎src/providers/registry.ts‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,12 @@ export interface ProviderRegistryEntry {
214214
modelReasoningEffortMap?: Record<string, Record<string, string>>;
215215
reasoningWireFormat?: CodexCommanderProviderConfig["reasoningWireFormat"];
216216
noVisionModels?: string[];
217+
/**
218+
* Models whose xAI Responses endpoint accepts the server-side `x_search` hosted tool. Routed chat
219+
* turns on these models get a synthetic `x_search` function tool that the proxy executes against
220+
* `/v1/responses` (see src/web-search/xai-x-search.ts). Registry-only: never seeded into config.
221+
*/
222+
xSearchModels?: string[];
217223
noReasoningModels?: string[];
218224
noTemperatureModels?: string[];
219225
noTopPModels?: string[];
@@ -941,7 +947,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
941947
// grok-composer-2.5-fast is retained as account-verified despite being absent from public docs.
942948
// grok-4.20-multi-agent-0309 is intentionally absent: the OAuth chat-completions
943949
// transport returns 400 ("Multi Agent requests are not allowed on chat completions").
944-
models: ["grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
950+
models: ["grok-4.7", "grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning", "grok-4.20-0309-non-reasoning", "grok-build-0.1", "grok-composer-2.5-fast"],
945951
defaultModel: "grok-4.5",
946952
// Vision lineup per docs.x.ai model-capabilities/images/understanding: the grok-4.x chat
947953
// models accept image input (JPEG/PNG, URL or base64). Without this the catalog leaves
@@ -950,6 +956,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
950956
// the app blocks attachments client-side. grok-build-0.1 / grok-composer-2.5-fast stay out
951957
// (they are already listed in noVisionModels below).
952958
modelInputModalities: {
959+
"grok-4.7": ["text", "image"],
953960
"grok-4.6": ["text", "image"],
954961
"grok-4.5": ["text", "image"],
955962
"grok-4.3": ["text", "image"],
@@ -961,7 +968,7 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
961968
// reasoning_content as the top cause of prompt-cache misses on multi-turn conversations
962969
// (docs.x.ai prompt-caching/multi-turn, verified 2026-07-13).
963970
// Models that never emit reasoning simply have no thinking parts to replay (no-op).
964-
preserveReasoningContentModels: ["grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning"],
971+
preserveReasoningContentModels: ["grok-4.7", "grok-4.6", "grok-4.5", "grok-4.3", "grok-4.20-0309-reasoning"],
965972
// grok reasoning is always-on with low/medium/high control (no off tier upstream);
966973
// grok-4.6+ extends the ladder with xhigh per docs.x.ai. xAI rejects max/ultra outright
967974
// (400 "Invalid reasoning effort"), so every grok reasoning model clamps ultra/max down
@@ -970,11 +977,13 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
970977
modelReasoningEfforts: {
971978
"grok-4.5": ["low", "medium", "high"],
972979
"grok-4.6": ["low", "medium", "high", "xhigh"],
980+
"grok-4.7": ["low", "medium", "high", "xhigh"],
973981
},
974982
// Provider default for live-discovered reasoning models: clamp to the verified xAI
975983
// ladder unless a per-model entry raises it (noReasoningModels stay effort-free).
976984
reasoningEfforts: ["low", "medium", "high"],
977985
modelContextWindows: {
986+
"grok-4.7": 500_000,
978987
"grok-4.6": 500_000,
979988
"grok-4.5": 500_000,
980989
"grok-4.3": 1_000_000,
@@ -983,6 +992,9 @@ export const PROVIDER_REGISTRY: readonly ProviderRegistryEntry[] = [
983992
"grok-build-0.1": 256_000,
984993
},
985994
noVisionModels: ["grok-build-0.1", "grok-composer-2.5-fast"],
995+
// Server-side X search (docs.x.ai/developers/tools/x-search) exists only on /v1/responses.
996+
// Live-verified 2026-09-25 for grok-4.7 on both the Grok-login CLI proxy and api.x.ai.
997+
xSearchModels: ["grok-4.7"],
986998
},
987999
{
9881000
id: "command-code",

‎src/server/responses/core.ts‎

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ import {
6565
resolveAnthropicAccountForSession,
6666
rotateAnthropicAccountOn429,
6767
} from "../../oauth/anthropic-routing";
68-
import { buildWebSearchTool, planWebSearch, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar } from "../../web-search";
68+
import { buildWebSearchTool, buildXSearchTool, planWebSearch, planXaiXSearch, planXSearchOnlyLoop, runWithWebSearch, shouldResolveOpenAiWebSearchSidecar, X_SEARCH_TOOL_NAME } from "../../web-search";
6969
import { describeImagesInPlace, planVisionSidecar, resolveOpenAiVisionModel, shouldResolveOpenAiVisionSidecar, stripImagesInPlace } from "../../vision";
7070
import { createAdapterEventQueue, preflightAdapterEvents } from "../../adapters/run-turn-queue";
7171
import {
@@ -2539,27 +2539,41 @@ async function handleResponsesInner(
25392539
const wsPlan = !routedCompaction
25402540
? planWebSearch(config, parsed, false, route.provider, route.modelId, openAiSidecar)
25412541
: undefined;
2542+
// xAI hosted X search: Grok models listed in the registry `xSearchModels` get a synthetic x_search
2543+
// function the loop runs on xAI /v1/responses (same transport + credential as this routed turn).
2544+
const xPlan = !routedCompaction && !adapter.runTurn
2545+
? planXaiXSearch(config, parsed, route.providerName, route.provider, route.modelId)
2546+
: undefined;
2547+
const xOnlyLoopPlan = xPlan ? planXSearchOnlyLoop(config, xPlan.timeoutMs) : undefined;
2548+
const loopPlan = wsPlan ?? xOnlyLoopPlan;
25422549
// Web-search sidecar: Codex enabled web_search but this is a routed (non-OpenAI) model that can't
25432550
// run it server-side. Expose web_search as a function tool and run searches via the gpt-mini sidecar
25442551
// through the ChatGPT passthrough, looping until the model answers. Otherwise take the normal path.
25452552
// Placed BEFORE the runTurn early-return for non-runTurn adapters so dual-tool turns dispatch
25462553
// through web-search instead of being swallowed. runTurn adapters never enter this branch.
2547-
if (wsPlan && !adapter.runTurn) {
2548-
const tools = parsed.context.tools ?? [];
2549-
if (!tools.some(tool => !tool.namespace && tool.name === "web_search")) {
2550-
parsed.context.tools = [...tools, buildWebSearchTool()];
2554+
if (loopPlan && !adapter.runTurn) {
2555+
if (xPlan) parsed.options.parallelToolCalls = false;
2556+
let tools = parsed.context.tools ?? [];
2557+
if (wsPlan && !tools.some(tool => !tool.namespace && tool.name === "web_search")) {
2558+
tools = [...tools, buildWebSearchTool()];
2559+
}
2560+
if (xPlan && !tools.some(tool => !tool.namespace && tool.name === X_SEARCH_TOOL_NAME)) {
2561+
tools = [...tools, buildXSearchTool()];
25512562
}
2563+
parsed.context.tools = tools;
25522564
const wsResponse = await runWithWebSearch({
25532565
parsed, adapter,
25542566
incomingMeta: { headers: selectedForwardHeaders, abortSignal: options.abortSignal, translatorBudget, dispatch: requestDispatchContext(logCtx, options.abortSignal ?? req.signal) },
25552567
toolParameterSchemas: toolBridgeMaps.toolParameterSchemas,
2556-
backend: wsPlan.backend,
2557-
forwardProvider: wsPlan.forwardSidecar?.provider,
2558-
anthropicSidecar: wsPlan.anthropicSidecar,
2559-
hostedTool: wsPlan.hostedTool,
2560-
selectedForwardHeaders: wsPlan.forwardSidecar?.headers ?? selectedForwardHeaders,
2561-
settings: wsPlan.settings,
2562-
maxSearches: wsPlan.maxSearches,
2568+
backend: loopPlan.backend,
2569+
forwardProvider: loopPlan.forwardSidecar?.provider,
2570+
anthropicSidecar: loopPlan.anthropicSidecar,
2571+
hostedTool: loopPlan.hostedTool,
2572+
selectedForwardHeaders: loopPlan.forwardSidecar?.headers ?? selectedForwardHeaders,
2573+
settings: loopPlan.settings,
2574+
maxSearches: loopPlan.maxSearches,
2575+
...(xPlan ? { xSearch: xPlan } : {}),
2576+
webSearchEnabled: !!wsPlan,
25632577
forceEmptyResponseId: true,
25642578
abortSignal: options.abortSignal,
25652579
...(options.onFirstOutput ? { onFirstOutput: options.onFirstOutput } : {}),
@@ -2573,10 +2587,10 @@ async function handleResponsesInner(
25732587
if (logCtx.activeAttempt) logCtx.activeAttempt.usage = usage;
25742588
}
25752589
},
2576-
recordSidecarOutcome: wsPlan.forwardSidecar?.recordOutcome,
2590+
recordSidecarOutcome: loopPlan.forwardSidecar?.recordOutcome,
25772591
connectTimeoutMs: config.connectTimeoutMs ?? 200_000,
2578-
routedModelStallTimeoutMs: wsPlan.routedModelStallTimeoutMs,
2579-
stallTimeoutSec: wsPlan.stallTimeoutSec,
2592+
routedModelStallTimeoutMs: loopPlan.routedModelStallTimeoutMs,
2593+
stallTimeoutSec: Math.max(loopPlan.stallTimeoutSec, xOnlyLoopPlan?.stallTimeoutSec ?? 0),
25802594
on429: retryAfter => {
25812595
const rotated = rotateProviderTransportOn429(config, route.providerName, route.provider, {
25822596
retryAfter,
@@ -2586,6 +2600,8 @@ async function handleResponsesInner(
25862600
});
25872601
if (!rotated) return null;
25882602
route.provider = rotated;
2603+
// Keep the X-search sidecar on the rotated credential too.
2604+
if (xPlan) xPlan.provider = rotated;
25892605
return resolveAdapter(
25902606
resolveWireProtocolOverride(route.providerName, route.modelId, route.provider, inboundWire),
25912607
config.cacheRetention,

‎src/types.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ export interface CodexCommanderTool {
166166
loadedFromToolSearch?: boolean;
167167
/** Synthetic web_search tool: the model's call is executed by the gpt-5.4-mini sidecar, not relayed to Codex. */
168168
webSearch?: boolean;
169+
/** Synthetic xAI x_search tool: executed by the proxy against xAI /v1/responses, not relayed to Codex. */
170+
xSearch?: boolean;
169171
}
170172

171173
/**
@@ -953,6 +955,11 @@ export interface CodexCommanderWebSearchSidecarConfig {
953955
maxSearchesPerTurn?: number;
954956
/** Sidecar fetch timeout (ms). */
955957
timeoutMs?: number;
958+
/**
959+
* xAI X search for Grok models listed in the registry `xSearchModels`. Default on: the routed
960+
* Grok model gets an `x_search` tool it calls only for X research. Set false to hide it.
961+
*/
962+
xSearch?: boolean;
956963
/**
957964
* Config-file-only deadline (ms) for continuous routed-model response-body raw-byte inactivity
958965
* during a web-search turn. Default 200000. Must be an integer from 1 through 2147483647.

‎src/web-search/index.ts‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,16 @@ import { getAccountSet } from "../oauth/store";
66
import { DEFAULT_STALL_TIMEOUT_SEC } from "../stall-timeout";
77

88
export { runWithWebSearch } from "./loop";
9+
export {
10+
buildXSearchTool,
11+
isXSearchModel,
12+
parseXSearchArgs,
13+
parseXSearchResponse,
14+
planXaiXSearch,
15+
runXaiXSearch,
16+
X_SEARCH_TOOL_NAME,
17+
type XaiXSearchSidecar,
18+
} from "./xai-x-search";
919
export { buildWebSearchTool, extractHostedWebSearch, WEB_SEARCH_TOOL_NAME } from "./synthetic-tool";
1020
export { runAnthropicWebSearch, parseAnthropicSidecarSSE } from "./anthropic-executor";
1121

@@ -194,3 +204,22 @@ export function planWebSearch(
194204
stallTimeoutSec,
195205
};
196206
}
207+
208+
/**
209+
* Loop settings for a turn that exposes ONLY the xAI x_search tool (Codex did not enable web_search,
210+
* or no web-search backend is usable). The web backend never runs because no web_search tool is
211+
* exposed; these values only bound the loop and its stall watchdog.
212+
*/
213+
export function planXSearchOnlyLoop(config: CodexCommanderConfig, xSearchTimeoutMs: number): SidecarPlan {
214+
const cfg = config.webSearchSidecar ?? {};
215+
const routedModelStallTimeoutMs = resolveRoutedModelStallTimeoutMs(cfg.routedModelStallTimeoutMs);
216+
const connectTimeoutMs = config.connectTimeoutMs ?? 200_000;
217+
return {
218+
backend: "openai",
219+
hostedTool: {},
220+
settings: { model: cfg.model ?? DEFAULT_SIDECAR_MODEL, reasoning: cfg.reasoning ?? DEFAULT_SIDECAR_REASONING, timeoutMs: xSearchTimeoutMs, describeImages: false },
221+
maxSearches: cfg.maxSearchesPerTurn ?? DEFAULT_MAX_SEARCHES,
222+
routedModelStallTimeoutMs,
223+
stallTimeoutSec: webSearchStallTimeoutSec(config.stallTimeoutSec, connectTimeoutMs, routedModelStallTimeoutMs, xSearchTimeoutMs),
224+
};
225+
}

0 commit comments

Comments
 (0)