# Provider clients

> Cursor default path, Claude Agent SDK query, Codex Responses at chatgpt.com/backend-api/codex/responses, OpenRouter OpenAI-compatible model default openai/gpt-5.2.

- Repository: sashimikun/grok-bot-0.18-reconstructed
- GitHub: https://github.com/sashimikun/grok-bot-0.18-reconstructed
- Human docs: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15
- Complete Markdown: https://grok-wiki.com/public/docs/sashimikun-grok-bot-0-18-reconstructed-c774cc9a5c15/llms-full.txt

## Source Files

- `source/host/extensions/inference/provider-session.ts`
- `source/host/extensions/inference/codex-direct-responses.ts`
- `source/shared/node/inference-router-local.ts`
- `source/host/extensions/inference/cursor-session.ts`
- `tests/codex-direct-responses.test.mjs`
- `tests/inference-router-transcript.test.mjs`

---

---
title: "Provider clients"
description: "Cursor default path, Claude Agent SDK query, Codex Responses at chatgpt.com/backend-api/codex/responses, OpenRouter OpenAI-compatible model default openai/gpt-5.2."
---

`createHostInference` and `createCoordinatorInferenceRouter` both read `inferenceProvider` from `settings.json` (default `cursor`). Cursor turns stay on the signed-in Connect RPC `InferenceService` at `https://api2.cursor.sh`. Non-cursor turns go through `runRoutedProviderText` / `createProviderPromptSession` in `provider-session.ts`: Claude Agent SDK `query`, Codex SSE `POST https://chatgpt.com/backend-api/codex/responses`, or OpenRouter `https://openrouter.ai/api/v1` with default model `openai/gpt-5.2`.

<Note>
Routed chat with Grok Bot plugins is the coordinator `sendPrompt` path. Host prompt sessions (`createProviderPromptSession`) stream text and record usage but do not attach an MCP executor.
</Note>

## Provider matrix

| `SandInferenceProvider` | Transport | Auth | Default model id | Plugin tools |
| --- | --- | --- | --- | --- |
| `cursor` | Connect RPC `aiserver.v1.InferenceService` | Grok Bot / Cursor access token | `grok-4.5` (`maxMode: true`, `effort=high`, `fast=true`) | Native host tools (not the routed MCP loop) |
| `claude-code` | `@anthropic-ai/claude-agent-sdk` `query` | Local Claude Code CLI + `~/.claude/.credentials.json` or `ANTHROPIC_API_KEY` | `claude-code` (`SAND_CLAUDE_MODEL` optional) | HTTP MCP bridge `grok_bot_plugins` |
| `codex` | Direct Responses SSE | Private `auth.json` ChatGPT tokens (CLI binary is **not** on the request path) | `gpt-5.4` | Direct `executeRoutedMcpTool` |
| `openrouter` | Vercel AI SDK `streamText` + `@ai-sdk/openai` | `OPENROUTER_API_KEY` env or `box-secrets.json` | `openai/gpt-5.2` | Direct `executeRoutedMcpTool` |

`SAND_INFERENCE_PROVIDERS` is the closed set `cursor`, `claude-code`, `codex`, `openrouter`. Unknown values fail `isSandInferenceProvider` and desktop `setInferenceRouter`.

```mermaid
flowchart TB
  subgraph ui [Settings Router]
    settings["settings.json inferenceProvider"]
    secrets["box-secrets.json OPENROUTER_API_KEY"]
  end
  subgraph coord [node-agent-coordinator]
    dispatch["createCoordinatorInferenceRouter.dispatch"]
    sendPrompt["sendPrompt when provider != cursor"]
    mcpBridge["createRoutedMcpBridge 127.0.0.1 MCP HTTP"]
    runText["runRoutedProviderText"]
  end
  subgraph host [host inference]
    createHost["createHostInference.createSession"]
    cursorSess["createCursorInferencePromptSession"]
    providerSess["createProviderPromptSession"]
  end
  subgraph clients [Provider clients]
    claudeQ["queryClaude Claude Agent SDK"]
    codexSSE["streamCodexDirectResponses"]
    orChat["createOpenAI OpenRouter chat"]
  end
  subgraph external [External endpoints]
    cursorApi["https://api2.cursor.sh"]
    claudeBin["claude executable"]
    codexApi["https://chatgpt.com/backend-api/codex/responses"]
    orApi["https://openrouter.ai/api/v1"]
    oauth["https://auth.openai.com/oauth/token"]
  end
  settings --> dispatch
  settings --> createHost
  dispatch --> sendPrompt
  sendPrompt --> runText
  runText --> claudeQ
  runText --> codexSSE
  runText --> orChat
  sendPrompt --> mcpBridge
  mcpBridge --> claudeQ
  createHost -->|cursor| cursorSess
  createHost -->|claude-code / codex / openrouter| providerSess
  cursorSess --> cursorApi
  claudeQ --> claudeBin
  codexSSE --> codexApi
  codexSSE -->|401 refresh| oauth
  orChat --> orApi
  secrets --> orChat
```

## Shared routed contract

All three non-cursor clients inject the same system instructions (`GROK_ROUTER_SYSTEM_PROMPT`): the model is Grok Bot, not Codex CLI or Claude Code, and must use already-connected plugin tools instead of asking for keys.

Tool-loop limits:

| Path | No tools | With tools |
| --- | --- | --- |
| Codex `maxSteps` | `1` | `8` |
| OpenRouter `streamText` `maxSteps` | `1` | `8` |
| Claude Code `maxTurns` | `1` | `8` |

Successful usage is written through `SandSettingsStore.recordInferenceUsage` into `inferenceRouterUsage` (`schemaVersion: 1`): `requests`, `inputTokens`, `outputTokens`, `cacheReadTokens`, `cacheWriteTokens`, `lastUsedAt`.

Coordinator turns also persist `inference-router-transcript.json` (`schemaVersion: 2`, last 200 entries per agent). The first assistant delta is delayed **1200 ms** so the shipped transcript can show composing state.

## Cursor

Default route. `createCursorSandInference` / `createCursorInferencePromptSession` build a Connect client with `httpVersion: "1.1"` against `getConfiguredBackendUrl()`.

<ParamField body="SAND_BACKEND_URL" type="string">
Overrides the Cursor backend base URL. Falls back to `CURSOR_API_BASE_URL`, then `https://api2.cursor.sh`.
</ParamField>

<ParamField body="SAND_AGENT_MODEL" type="string">
Overrides the requested `modelId` for a Cursor session.
</ParamField>

<ParamField body="SAND_AGENT_MOCK_RESPONSE" type="string">
When set, skips the network: either a JSON mock script (`sendMessage` / `toolCalls`) or a chunked plain-text mock.
</ParamField>

Request headers on the Cursor transport:

| Header | Value |
| --- | --- |
| `authorization` | `Bearer <accessToken>` (deleted for `authMode: "anonymous"`) |
| `x-cursor-checksum` | `createCursorChecksum(machineId)` |
| `x-cursor-client-type` | `sand` |
| `x-cursor-client-version` | Stamped app version, with `-dev` / `-lab` suffixes from box namespace |
| `x-sand-box-namespace` | `dev` \| `lab` \| `prod` |
| `x-ghost-mode` | Derived from privacy-mode lookup; Dashboard `GetUserPrivacyMode` always sends `"true"` |
| `x-request-id` | Existing header or new UUID |
| `x-parent-request-id` / `x-root-parent-request-id` | Optional request lineage |

Default model selection is `{ modelId: "grok-4.5", maxMode: true, parameters: [{ id: "effort", value: "high" }, { id: "fast", value: "true" }] }`. Computer-use subagents default to `claude-opus-4-8`. Summarization, computer-use, and `skipLabeling` sessions skip follow-up labeling.

`sendPrompt` is **not** intercepted when the provider is `cursor`; the remote host transcript remains authoritative.

## Claude Code

`claudeExecutor` resolves a `claude` binary, then runs `query` from `@anthropic-ai/claude-agent-sdk`.

Executable search order (`resolveClaudeCodeCliPath`):

1. `CLAUDE_CODE_PATH`
2. `~/.local/bin/claude`
3. `~/.claude/local/claude`
4. `$PATH` entries named `claude`
5. `/opt/homebrew/bin/claude`
6. `/usr/local/bin/claude`

If none exist: `Claude Code is not installed. Install and sign in to Claude Code, then reopen Grok Bot.`

Local status (`getLocalInferenceCliStatus()["claude-code"]`):

| Field | Meaning |
| --- | --- |
| `installed` | Executable found |
| `authenticated` | `~/.claude/.credentials.json` exists **or** `ANTHROPIC_API_KEY` is non-empty |
| `executablePath` | Resolved binary |

`query` options:

| Option | Value |
| --- | --- |
| `pathToClaudeCodeExecutable` | Resolved CLI |
| `cwd` | Sand root (`getSandRootDir()`) |
| `permissionMode` | `"default"` |
| `persistSession` | `false` |
| `tools` | `[]` without MCP; `["mcp__grok_bot_plugins__*"]` with MCP |
| `mcpServers.grok_bot_plugins` | `{ type: "http", url: mcpServerUrl }` |
| `strictMcpConfig` | `true` when MCP is attached |
| `maxTurns` | `1` or `8` |
| `model` | `SAND_CLAUDE_MODEL` when non-empty |

The coordinator attaches `createRoutedMcpBridge`: a loopback HTTP JSON-RPC server at `http://127.0.0.1:<ephemeral>/mcp/<secret>` (`protocolVersion: "2025-03-26"`, `serverInfo.name: "grok-bot-plugins"`). `tools/list` and `tools/call` proxy `listRoutedMcpTools` / `executeRoutedMcpTool`. Body size cap is 1 MiB.

A result message with `subtype !== "success"` throws joined `errors` or `Claude Code failed (<subtype>).`. Empty stream: `Claude Code ended without a result.` Provider metadata stores `{ anthropic: { sessionId, totalCostUsd } }`. The prompt-session `modelId` is the literal `"claude-code"`.

## Codex

Grok Bot does **not** spawn the Codex CLI for inference. `getLocalInferenceCliStatus().codex` still reports the binary for Settings status, but comments and code treat login as an HTTP credential store.

### Credentials

Path: `$CODEX_HOME/auth.json` or `~/.codex/auth.json`.

The file must be a regular file, not a symlink, with no group/other permission bits (`mode & 0o077 === 0`). Required fields:

- `auth_mode === "chatgpt"`
- `tokens.access_token`, `tokens.refresh_token`, `tokens.id_token`, `tokens.account_id` (all non-empty strings)

Otherwise: `Codex is not signed in with ChatGPT. Run \`codex login\`, then reopen Grok Bot.` Permission failures: `Codex login credentials must be a private direct regular file.`

On HTTP `401`, `refreshCodexCredentials` POSTs `grant_type=refresh_token` to `https://auth.openai.com/oauth/token` with `client_id` from the id-token JWT `aud`. The rewrite is atomic: `0o600` tempfile + `rename`. Retry uses `authorization: Bearer <accessToken>` and `ChatGPT-Account-Id`.

### Model

<ParamField body="SAND_CODEX_MODEL" type="string">
Overrides the Responses `model`. Else first `model = "..."` in `$CODEX_HOME/config.toml` / `~/.codex/config.toml`. Default `gpt-5.4`.
</ParamField>

<ParamField body="SAND_CODEX_REASONING_EFFORT" type='"minimal" \| "low" \| "medium" \| "high" \| "xhigh"'>
Overrides reasoning effort. Else `model_reasoning_effort` in `config.toml`. Omitted when unset.
</ParamField>

`resolveCodexCliPath` (status only): `CODEX_PATH`, `~/.local/bin/codex`, `~/.codex/bin/codex`, `$PATH`, `/opt/homebrew/bin/codex`, `/usr/local/bin/codex`. `installed` is true when `auth.json` exists, not when the binary exists.

### Responses transport

`streamCodexDirectResponses` owns the SSE reader. Default `maxSteps` is `8`.

:::endpoint POST https://chatgpt.com/backend-api/codex/responses
Stream one Codex Responses turn. Grok Bot sets `user-agent: grok-bot-router/1`, `accept: text/event-stream`, `content-type: application/json`, plus the ChatGPT bearer headers.

**Body**

- `model` — configured Codex model
- `instructions` — `GROK_ROUTER_SYSTEM_PROMPT`
- `input` — conversation roles coerced to `user` | `assistant`
- `tools` — `{ type: "function", name, description?, parameters, strict: false }` when tools exist
- `tool_choice` — `"auto"` when tools exist
- `parallel_tool_calls` — `true` when tools exist
- `reasoning` — `{ effort, summary: "auto" }` when effort is configured
- `include` — `["reasoning.encrypted_content"]`
- `stream` — `true`
- `store` — `false`

**SSE events consumed**

- `response.output_text.delta` → `{ type: "text-delta", delta }`
- `response.output_item.done` — captured for tool-call fallback when `response.output` is empty
- `response.completed` — required; missing it throws `Codex direct response ended without response.completed.`
- `response.failed` / `error` — fail closed

**Tool continuation**

Function calls keep the provider `call_id`. Outputs are `{ type: "function_call_output", call_id, output }` JSON. Unknown tools and executor exceptions return `{ isError: true, error }` instead of aborting the loop. Exceeding `maxSteps` throws `Codex exceeded Grok Bot's N-step tool limit.` Truncated SSE throws `Codex direct response ended with an incomplete SSE event.`
:::

Usage maps `input_tokens`, `output_tokens`, and `input_tokens_details.cached_tokens` (`cacheWriteTokens` is always `0`). Provider metadata is `{ openai: { responseId, direct: true } }`.

```mermaid
sequenceDiagram
  participant Coord as Coordinator sendPrompt
  participant Exec as codexExecutor
  participant API as chatgpt.com/.../codex/responses
  participant Auth as auth.openai.com/oauth/token
  participant MCP as executeRoutedMcpTool
  Coord->>Exec: runRoutedProviderText(codex, messages, tools)
  Exec->>API: POST stream store=false
  alt HTTP 401
    Exec->>Auth: refresh_token
    Exec->>API: retry with new Bearer
  end
  API-->>Exec: response.output_text.delta / function_call
  Exec->>MCP: executeTool(source, args, call_id)
  MCP-->>Exec: tool result JSON
  Exec->>API: input plus function_call_output
  API-->>Exec: response.completed
  Exec-->>Coord: done text + usage
```

## OpenRouter

`openRouterExecutor` builds `createOpenAI({ apiKey, baseURL: "https://openrouter.ai/api/v1", compatibility: "compatible", name: "openrouter" })`.

<ParamField body="OPENROUTER_API_KEY" type="string" required>
Read from the process environment first, then `box-secrets.json` `secrets.OPENROUTER_API_KEY`. Missing: `OpenRouter needs OPENROUTER_API_KEY. Add it in Settings → Router.`
</ParamField>

<ParamField body="SAND_OPENROUTER_MODEL" type="string">
OpenRouter model id. Default `openai/gpt-5.2`.
</ParamField>

Fixed client headers:

| Header | Value |
| --- | --- |
| `HTTP-Referer` | `https://github.com/grok-bot-reconstructed` |
| `X-Title` | `Grok Bot Reconstructed` |

`streamText` uses the shared system prompt, `toolCallStreaming: true`, and JSON-schema tools from `inputSchema` or `parameters`. Cache token fields are recorded as `0`. Settings UI upserts the key through `window.desktop.secrets.upsert({ OPENROUTER_API_KEY })`.

## Coordinator vs host session

<Tabs>
<Tab title="Chat sendPrompt">
When `inferenceProvider` is not `cursor`, `createCoordinatorInferenceRouter.dispatch("sendPrompt")` queues per `agentId`, appends a local user row (`t{turn}u`), waits 1200 ms, then:

- **claude-code** — `mcpServerUrl` from the loopback MCP bridge
- **codex / openrouter** — `listRoutedMcpTools` plus `executeRoutedMcpTool` (`providerIdentifier`, `name`, `toolName`, `args`, `toolCallId`, `agentId`)

Failures append `Router error: <message>` as an assistant row instead of throwing to the renderer. Requires non-empty `agentId` and `prompt`.
</Tab>
<Tab title="Host createSession">
`createHostInference.createSession` (and summarization) uses Cursor Connect when the setting is `cursor`, otherwise `createProviderPromptSession`. The same routed split exists inside `createCursorSandInference` and `createCursorInferencePromptSession` so nested Cursor session builders cannot ignore the router. Host routed streams pass tool definitions without `executeTool` / MCP URL.
</Tab>
</Tabs>

## Environment and files

| Name | Used by |
| --- | --- |
| `OPENROUTER_API_KEY` | OpenRouter credential |
| `SAND_OPENROUTER_MODEL` | OpenRouter model (default `openai/gpt-5.2`) |
| `SAND_CLAUDE_MODEL` | Claude Agent SDK `model` |
| `CLAUDE_CODE_PATH` | Claude CLI override |
| `ANTHROPIC_API_KEY` | Claude status `authenticated` |
| `CODEX_HOME` | Codex `auth.json` / `config.toml` directory |
| `SAND_CODEX_MODEL` | Codex Responses model (default `gpt-5.4`) |
| `SAND_CODEX_REASONING_EFFORT` | Codex `reasoning.effort` |
| `CODEX_PATH` | Codex CLI path for Settings status only |
| `SAND_BACKEND_URL` / `CURSOR_API_BASE_URL` | Cursor Connect base URL |
| `SAND_AGENT_MODEL` | Cursor requested model |
| `SAND_AGENT_MOCK_RESPONSE` | Cursor mock executor |

Credential files:

| Path | Constraint |
| --- | --- |
| `$CODEX_HOME/auth.json` | Private regular file, `auth_mode: "chatgpt"` |
| `$CODEX_HOME/config.toml` | Optional `model` and `model_reasoning_effort` |
| `~/.claude/.credentials.json` | Claude authenticated flag |
| `<sandRoot>/box-secrets.json` | Persisted `OPENROUTER_API_KEY` |
| `<sandRoot>/settings.json` | `inferenceProvider`, `inferenceRouterUsage` |
| `<dataDir>/inference-router-transcript.json` | Routed chat store, mode `0o600` |

## Failure strings

| Client | Error |
| --- | --- |
| Claude Code | `Claude Code is not installed. Install and sign in to Claude Code, then reopen Grok Bot.` |
| Claude Code | `Claude Code ended without a result.` / `Claude Code failed (<subtype>).` |
| Codex | `Codex login credentials must be a private direct regular file.` |
| Codex | `Codex is not signed in with ChatGPT. Run \`codex login\`, then reopen Grok Bot.` |
| Codex | `Codex login expired and could not be refreshed. Run \`codex login\` again.` |
| Codex | `Codex requested a tool but Grok Bot did not provide an executor.` |
| Codex | `Codex direct request failed (<status>: …).` |
| OpenRouter | `OpenRouter needs OPENROUTER_API_KEY. Add it in Settings → Router.` |
| Coordinator | `Local inference routing requires an agentId and prompt` |
| Desktop RPC | `Unknown inference provider.` |

## Next

<CardGroup>
<Card title="Inference router" href="/inference-router">
Provider ids, default `cursor`, transcript `schemaVersion` 2, usage `schemaVersion` 1.
</Card>
<Card title="Choose an inference provider" href="/choose-inference-provider">
Settings → Router, persist `inferenceProvider`, and satisfy each client’s login.
</Card>
<Card title="Route Grok Bot plugin tools" href="/route-mcp-tools">
Claude MCP bridge versus Codex/OpenRouter direct `executeRoutedMcpTool`.
</Card>
<Card title="Environment variables" href="/environment-variables">
`OPENROUTER_API_KEY`, `SAND_OPENROUTER_MODEL`, `CODEX_HOME`, `CLAUDE_CODE_PATH`, Cursor backend overrides.
</Card>
<Card title="Router and sandbox failures" href="/router-failures">
Unknown provider, private `auth.json`, missing Claude CLI, missing OpenRouter key.
</Card>
<Card title="Desktop RPC" href="/desktop-rpc">
`getInferenceRouter` / `setInferenceRouter` and secrets upsert for the OpenRouter key.
</Card>
</CardGroup>
