llm-cli-gateway 2.3.0 → 2.4.0
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.
- package/CHANGELOG.md +70 -9
- package/README.md +1 -1
- package/dist/config.d.ts +17 -0
- package/dist/config.js +84 -0
- package/dist/executor.js +17 -21
- package/dist/flight-recorder.d.ts +2 -1
- package/dist/index.d.ts +26 -6
- package/dist/index.js +608 -54
- package/dist/metrics.d.ts +3 -3
- package/dist/metrics.js +8 -8
- package/dist/request-helpers.d.ts +8 -8
- package/dist/resources.js +56 -7
- package/dist/session-manager-pg.d.ts +6 -6
- package/dist/session-manager-pg.js +1 -0
- package/dist/session-manager.d.ts +16 -12
- package/dist/session-manager.js +4 -1
- package/dist/upstream-contracts.d.ts +84 -0
- package/dist/upstream-contracts.js +698 -6
- package/dist/xai-api-provider.d.ts +43 -0
- package/dist/xai-api-provider.js +191 -0
- package/migrations/001_initial_schema.sql +65 -0
- package/migrations/002_session_ids_as_text.sql +26 -0
- package/migrations/003_provider_type_sessions.sql +20 -0
- package/npm-shrinkwrap.json +2 -2
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -12,12 +12,13 @@ import { parseCodexJsonStream } from "./codex-json-parser.js";
|
|
|
12
12
|
import { parseGeminiJson, parseGeminiStreamJson } from "./gemini-json-parser.js";
|
|
13
13
|
import { parseVibeMetaJson } from "./mistral-meta-json-parser.js";
|
|
14
14
|
import { homedir } from "os";
|
|
15
|
-
import { createSessionManager } from "./session-manager.js";
|
|
15
|
+
import { CLI_TYPES, PROVIDER_TYPES, createSessionManager, } from "./session-manager.js";
|
|
16
16
|
import { createWorktree, createWorktreeSessionCleanupHook, } from "./worktree-manager.js";
|
|
17
17
|
import { ResourceProvider } from "./resources.js";
|
|
18
18
|
import { PerformanceMetrics } from "./metrics.js";
|
|
19
19
|
import { estimateTokens, optimizePrompt as optimizePromptText, optimizeResponse as optimizeResponseText, } from "./optimizer.js";
|
|
20
|
-
import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, minStableTokensForModel, } from "./config.js";
|
|
20
|
+
import { loadConfig, loadPersistenceConfig, loadCacheAwarenessConfig, loadProvidersConfig, isXaiProviderEnabled, minStableTokensForModel, } from "./config.js";
|
|
21
|
+
import { createXaiResponse, XaiApiError, } from "./xai-api-provider.js";
|
|
21
22
|
import { checkHealth } from "./health.js";
|
|
22
23
|
import { clearModelRegistryCache, getAvailableCliInfo, getCliInfo, resolveModelAlias, } from "./model-registry.js";
|
|
23
24
|
import { AsyncJobManager, } from "./async-job-manager.js";
|
|
@@ -33,7 +34,7 @@ import { getCliVersions, runCliUpgrade } from "./cli-updater.js";
|
|
|
33
34
|
import { startHttpGateway } from "./http-transport.js";
|
|
34
35
|
import { printDoctorJson } from "./doctor.js";
|
|
35
36
|
import { registerValidationTools } from "./validation-tools.js";
|
|
36
|
-
import { assertUpstreamCliArgs, assertUpstreamCliEnv, buildUpstreamContractReport, } from "./upstream-contracts.js";
|
|
37
|
+
import { assertUpstreamCliArgs, assertUpstreamCliEnv, buildProviderSubcommandsCompactCatalog, buildUpstreamContractReport, getCliSubcommandContract, probeInstalledCliContract, serializeCliSubcommandContract, } from "./upstream-contracts.js";
|
|
37
38
|
import { entrypointFileURL } from "./entrypoint-url.js";
|
|
38
39
|
const logger = {
|
|
39
40
|
info: (message, ...args) => {
|
|
@@ -141,31 +142,32 @@ function loadSkills() {
|
|
|
141
142
|
return skills;
|
|
142
143
|
}
|
|
143
144
|
const loadedSkills = loadSkills();
|
|
144
|
-
export function buildServerInstructions(asyncJobsEnabled) {
|
|
145
|
+
export function buildServerInstructions(asyncJobsEnabled, grokApiToolsEnabled = false) {
|
|
145
146
|
const asyncToolsNote = asyncJobsEnabled ? " | *_request_async (async)" : "";
|
|
147
|
+
const apiToolsNote = grokApiToolsEnabled ? ", grok_api_request" : "";
|
|
146
148
|
const jobsLine = asyncJobsEnabled ? "Jobs: llm_job_status, llm_job_result, llm_job_cancel\n" : "";
|
|
147
149
|
const deferralLine = asyncJobsEnabled
|
|
148
150
|
? `- Sync auto-defers at ${SYNC_DEADLINE_MS}ms. Poll deferred jobs via llm_job_status/llm_job_result.`
|
|
149
151
|
: '- Async jobs are DISABLED (persistence.backend = "none"): *_request_async and llm_job_* tools are not registered, and sync requests run to completion (no auto-deferral).';
|
|
150
152
|
return `llm-cli-gateway: Multi-LLM orchestration via MCP.
|
|
151
153
|
|
|
152
|
-
Tools: claude_request, codex_request, gemini_request, grok_request, mistral_request (sync)${asyncToolsNote} | codex_fork_session (fork a Codex session into a new branch)
|
|
154
|
+
Tools: claude_request, codex_request, gemini_request, grok_request, mistral_request${apiToolsNote} (sync)${asyncToolsNote} | codex_fork_session (fork a Codex session into a new branch)
|
|
153
155
|
Validation: validate_with_models, second_opinion, compare_answers, red_team_review, consensus_check, ask_model, synthesize_validation, list_available_models | job_status/job_result (validation jobs)
|
|
154
156
|
${jobsLine}Sessions: session_create, session_list, session_set_active, session_get, session_delete, session_clear_all
|
|
155
|
-
Other: list_models, cli_versions, upstream_contracts (
|
|
157
|
+
Other: list_models, cli_versions, upstream_contracts, provider_subcommands_* (read-only subcommand contract/drift introspection), cli_upgrade, approval_list, llm_process_health, llm_request_result (read back any persisted request — sync or async — by correlationId)
|
|
156
158
|
|
|
157
159
|
Key behaviors:
|
|
158
160
|
${deferralLine}
|
|
159
161
|
- Sessions: Claude --continue, Gemini --resume, Grok --resume/--continue, Mistral --resume/--continue (current Vibe defaults session logging on; doctor flags explicit session_logging.enabled=false), Codex \`exec resume <ID>\` / \`exec resume --last\` (all real CLI continuity). For Codex, sessionId must be a real Codex UUID (from ~/.codex/sessions/); gateway-generated gw-* IDs are rejected.
|
|
160
162
|
- Approval gates: opt-in via approvalStrategy:"mcp_managed".
|
|
161
|
-
- Upstream drift detection: After upgrading any provider CLI (especially grok), use
|
|
163
|
+
- Upstream drift detection: After upgrading any provider CLI (especially grok), use upstream_contracts with probeInstalled:true and provider_subcommand_drift for declared subcommand help surfaces. Probes are safe, read-only --help checks.
|
|
162
164
|
- Idle timeout kills stuck processes (default 10min, configurable via idleTimeoutMs).
|
|
163
165
|
|
|
164
166
|
Skills (full docs via MCP resources):
|
|
165
167
|
${loadedSkills.map(s => `- skills://${s.name} — ${s.description}`).join("\n")}`;
|
|
166
168
|
}
|
|
167
|
-
function newGatewayMcpServer(asyncJobsEnabled = true) {
|
|
168
|
-
return new McpServer({ name: "llm-cli-gateway", version: packageVersion() }, { instructions: buildServerInstructions(asyncJobsEnabled) });
|
|
169
|
+
function newGatewayMcpServer(asyncJobsEnabled = true, grokApiToolsEnabled = false) {
|
|
170
|
+
return new McpServer({ name: "llm-cli-gateway", version: packageVersion() }, { instructions: buildServerInstructions(asyncJobsEnabled, grokApiToolsEnabled) });
|
|
169
171
|
}
|
|
170
172
|
let sessionManager;
|
|
171
173
|
let db = null;
|
|
@@ -174,6 +176,7 @@ let resourceProvider;
|
|
|
174
176
|
let flightRecorder = null;
|
|
175
177
|
let persistenceConfig = null;
|
|
176
178
|
let cacheAwarenessConfig = null;
|
|
179
|
+
let providersConfig = null;
|
|
177
180
|
let jobStore = null;
|
|
178
181
|
let jobStoreInitialized = false;
|
|
179
182
|
let asyncJobManager = null;
|
|
@@ -190,6 +193,10 @@ function getCacheAwarenessConfig(runtimeLogger = logger) {
|
|
|
190
193
|
cacheAwarenessConfig ??= loadCacheAwarenessConfig(runtimeLogger);
|
|
191
194
|
return cacheAwarenessConfig;
|
|
192
195
|
}
|
|
196
|
+
function getProvidersConfig(runtimeLogger = logger) {
|
|
197
|
+
providersConfig ??= loadProvidersConfig(runtimeLogger);
|
|
198
|
+
return providersConfig;
|
|
199
|
+
}
|
|
193
200
|
function getJobStore(runtimeLogger = logger) {
|
|
194
201
|
if (jobStoreInitialized)
|
|
195
202
|
return jobStore;
|
|
@@ -217,6 +224,7 @@ function getApprovalManager(runtimeLogger = logger) {
|
|
|
217
224
|
return approvalManager;
|
|
218
225
|
}
|
|
219
226
|
const MCP_SERVER_ENUM = z.enum(CLAUDE_MCP_SERVER_NAMES);
|
|
227
|
+
const CLI_TYPE_ENUM = z.enum(CLI_TYPES);
|
|
220
228
|
export const MAX_TURNS_SCHEMA = z.number().int().positive().safe().max(10_000);
|
|
221
229
|
export const MAX_TOKENS_SCHEMA = z.number().int().positive().safe().max(100_000_000);
|
|
222
230
|
export const MAX_PRICE_SCHEMA = z.number().positive().finite().min(1e-6).max(10_000);
|
|
@@ -244,7 +252,7 @@ export const WORKTREE_SCHEMA = z
|
|
|
244
252
|
"path. NOTE: callers should `.gitignore` the `.worktrees/` " +
|
|
245
253
|
"directory in their repo (the gateway does NOT auto-gitignore — " +
|
|
246
254
|
"see slice λ spec Q4).");
|
|
247
|
-
export const SESSION_PROVIDER_VALUES =
|
|
255
|
+
export const SESSION_PROVIDER_VALUES = PROVIDER_TYPES;
|
|
248
256
|
export const SESSION_PROVIDER_ENUM = z.enum(SESSION_PROVIDER_VALUES);
|
|
249
257
|
let activeServer = null;
|
|
250
258
|
let activeHttpGateway = null;
|
|
@@ -277,8 +285,12 @@ export function resolveGatewayServerRuntime(deps = {}, options = {}) {
|
|
|
277
285
|
logger: runtimeLogger,
|
|
278
286
|
persistence: deps.persistence ?? getPersistenceConfig(runtimeLogger),
|
|
279
287
|
cacheAwareness: deps.cacheAwareness ?? getCacheAwarenessConfig(runtimeLogger),
|
|
288
|
+
providers: deps.providers ?? getProvidersConfig(runtimeLogger),
|
|
280
289
|
};
|
|
281
290
|
}
|
|
291
|
+
export function shouldRegisterGrokApiTools(providers) {
|
|
292
|
+
return isXaiProviderEnabled(providers);
|
|
293
|
+
}
|
|
282
294
|
const CLI_IDLE_TIMEOUTS = {
|
|
283
295
|
claude: 600_000,
|
|
284
296
|
codex: 600_000,
|
|
@@ -741,12 +753,12 @@ function registerBaseResources(server, runtime) {
|
|
|
741
753
|
const contents = await runtime.resourceProvider.readResource(uri.href);
|
|
742
754
|
return { contents: contents ? [contents] : [] };
|
|
743
755
|
});
|
|
744
|
-
server.registerResource("cache-state-global", "
|
|
756
|
+
server.registerResource("cache-state-global", "cache-state://global", {
|
|
745
757
|
title: "💾 Cache State (Global)",
|
|
746
758
|
description: "Aggregate cache hit/miss/savings across all CLIs in the flight recorder. Tokens/hashes only — no prompt text.",
|
|
747
759
|
mimeType: "application/json",
|
|
748
760
|
}, async (uri) => {
|
|
749
|
-
runtime.logger.debug("Reading
|
|
761
|
+
runtime.logger.debug("Reading cache-state://global resource");
|
|
750
762
|
const stats = runtime.resourceProvider.readCacheStateGlobal({
|
|
751
763
|
lastNHours: 24,
|
|
752
764
|
});
|
|
@@ -760,7 +772,7 @@ function registerBaseResources(server, runtime) {
|
|
|
760
772
|
],
|
|
761
773
|
};
|
|
762
774
|
});
|
|
763
|
-
server.registerResource("cache-state-session", new ResourceTemplate("
|
|
775
|
+
server.registerResource("cache-state-session", new ResourceTemplate("cache-state://session/{sessionId}", { list: undefined }), {
|
|
764
776
|
title: "💾 Cache State (Session)",
|
|
765
777
|
description: "Per-session cache hit/miss/savings. Tokens/hashes only — no prompt text.",
|
|
766
778
|
mimeType: "application/json",
|
|
@@ -768,7 +780,7 @@ function registerBaseResources(server, runtime) {
|
|
|
768
780
|
const sessionId = Array.isArray(variables.sessionId)
|
|
769
781
|
? variables.sessionId[0]
|
|
770
782
|
: variables.sessionId;
|
|
771
|
-
runtime.logger.debug(`Reading
|
|
783
|
+
runtime.logger.debug(`Reading cache-state://session/${sessionId}`);
|
|
772
784
|
const stats = runtime.resourceProvider.readCacheStateSession(String(sessionId));
|
|
773
785
|
return {
|
|
774
786
|
contents: [
|
|
@@ -780,13 +792,13 @@ function registerBaseResources(server, runtime) {
|
|
|
780
792
|
],
|
|
781
793
|
};
|
|
782
794
|
});
|
|
783
|
-
server.registerResource("cache-state-prefix", new ResourceTemplate("
|
|
795
|
+
server.registerResource("cache-state-prefix", new ResourceTemplate("cache-state://prefix/{hash}", { list: undefined }), {
|
|
784
796
|
title: "💾 Cache State (Prefix)",
|
|
785
797
|
description: "Per-stable-prefix-hash cache hit/miss/savings, with CLI breakdown. Tokens/hashes only — no prompt text.",
|
|
786
798
|
mimeType: "application/json",
|
|
787
799
|
}, async (uri, variables) => {
|
|
788
800
|
const hash = Array.isArray(variables.hash) ? variables.hash[0] : variables.hash;
|
|
789
|
-
runtime.logger.debug(`Reading
|
|
801
|
+
runtime.logger.debug(`Reading cache-state://prefix/${hash}`);
|
|
790
802
|
const stats = runtime.resourceProvider.readCacheStateForPrefix(String(hash));
|
|
791
803
|
return {
|
|
792
804
|
contents: [
|
|
@@ -798,6 +810,30 @@ function registerBaseResources(server, runtime) {
|
|
|
798
810
|
],
|
|
799
811
|
};
|
|
800
812
|
});
|
|
813
|
+
server.registerResource("provider-subcommands-catalog", "provider-subcommands://catalog", {
|
|
814
|
+
title: "Provider Subcommands Catalog",
|
|
815
|
+
description: "Compact read-only catalog of declared provider CLI subcommands",
|
|
816
|
+
mimeType: "application/json",
|
|
817
|
+
}, async (uri) => {
|
|
818
|
+
runtime.logger.debug("Reading provider-subcommands://catalog resource");
|
|
819
|
+
const contents = await runtime.resourceProvider.readResource(uri.href);
|
|
820
|
+
return { contents: contents ? [contents] : [] };
|
|
821
|
+
});
|
|
822
|
+
server.registerResource("provider-subcommand-contract", new ResourceTemplate("provider-subcommands://{provider}/{+commandPath}", { list: undefined }), {
|
|
823
|
+
title: "Provider Subcommand Contract",
|
|
824
|
+
description: "Detailed read-only contract for one declared provider CLI subcommand",
|
|
825
|
+
mimeType: "application/json",
|
|
826
|
+
}, async (uri, variables) => {
|
|
827
|
+
const provider = Array.isArray(variables.provider)
|
|
828
|
+
? variables.provider[0]
|
|
829
|
+
: variables.provider;
|
|
830
|
+
const commandPath = Array.isArray(variables.commandPath)
|
|
831
|
+
? variables.commandPath[0]
|
|
832
|
+
: variables.commandPath;
|
|
833
|
+
runtime.logger.debug(`Reading provider-subcommands://${provider}/${commandPath}`);
|
|
834
|
+
const contents = await runtime.resourceProvider.readResource(uri.href);
|
|
835
|
+
return { contents: contents ? [contents] : [] };
|
|
836
|
+
});
|
|
801
837
|
}
|
|
802
838
|
function resolvePromptOrPartsForPrep(args) {
|
|
803
839
|
const hasPrompt = typeof args.prompt === "string" && args.prompt.length > 0;
|
|
@@ -1676,6 +1712,271 @@ function buildCliResponse(cli, stdout, optimizeResponse, corrId, sessionId, prep
|
|
|
1676
1712
|
}
|
|
1677
1713
|
return response;
|
|
1678
1714
|
}
|
|
1715
|
+
function buildXaiPromptPartsUserContent(promptParts) {
|
|
1716
|
+
const userSections = [];
|
|
1717
|
+
if (promptParts.tools && promptParts.tools.length > 0) {
|
|
1718
|
+
userSections.push(`<tools>\n${promptParts.tools}\n</tools>`);
|
|
1719
|
+
}
|
|
1720
|
+
if (promptParts.context && promptParts.context.length > 0) {
|
|
1721
|
+
userSections.push(`<context>\n${promptParts.context}\n</context>`);
|
|
1722
|
+
}
|
|
1723
|
+
if (promptParts.task && promptParts.task.length > 0) {
|
|
1724
|
+
userSections.push(promptParts.task);
|
|
1725
|
+
}
|
|
1726
|
+
return userSections.join("\n\n");
|
|
1727
|
+
}
|
|
1728
|
+
function buildXaiPromptPartsEffectivePrompt(instructions, userContent) {
|
|
1729
|
+
return instructions && instructions.length > 0
|
|
1730
|
+
? `${instructions}\n\n${userContent}`
|
|
1731
|
+
: userContent;
|
|
1732
|
+
}
|
|
1733
|
+
function prepareGrokApiRequest(params, providers) {
|
|
1734
|
+
const corrId = params.correlationId || randomUUID();
|
|
1735
|
+
if (!providers.xai) {
|
|
1736
|
+
return createErrorResponse("grok_api_request", 1, "", corrId, new Error("[providers.xai] is not configured"));
|
|
1737
|
+
}
|
|
1738
|
+
const inputResolution = resolvePromptOrPartsForPrep({
|
|
1739
|
+
prompt: params.prompt,
|
|
1740
|
+
promptParts: params.promptParts,
|
|
1741
|
+
operation: "grok_api_request",
|
|
1742
|
+
correlationId: corrId,
|
|
1743
|
+
});
|
|
1744
|
+
if (!inputResolution.ok)
|
|
1745
|
+
return inputResolution.error;
|
|
1746
|
+
const instructions = params.promptParts?.system && params.promptParts.system.length > 0
|
|
1747
|
+
? params.promptParts.system
|
|
1748
|
+
: undefined;
|
|
1749
|
+
let effectivePrompt = inputResolution.assembledPrompt;
|
|
1750
|
+
let input;
|
|
1751
|
+
if (params.promptParts) {
|
|
1752
|
+
let userContent = buildXaiPromptPartsUserContent(params.promptParts);
|
|
1753
|
+
if (params.optimizePrompt) {
|
|
1754
|
+
const optimized = optimizePromptText(userContent);
|
|
1755
|
+
logOptimizationTokens("prompt", corrId, userContent, optimized);
|
|
1756
|
+
userContent = optimized;
|
|
1757
|
+
}
|
|
1758
|
+
effectivePrompt = buildXaiPromptPartsEffectivePrompt(instructions, userContent);
|
|
1759
|
+
input = [{ role: "user", content: userContent }];
|
|
1760
|
+
}
|
|
1761
|
+
else {
|
|
1762
|
+
if (params.optimizePrompt) {
|
|
1763
|
+
const optimized = optimizePromptText(effectivePrompt);
|
|
1764
|
+
logOptimizationTokens("prompt", corrId, effectivePrompt, optimized);
|
|
1765
|
+
effectivePrompt = optimized;
|
|
1766
|
+
}
|
|
1767
|
+
input = effectivePrompt;
|
|
1768
|
+
}
|
|
1769
|
+
const resolvedModel = params.model ?? providers.xai.defaultModel;
|
|
1770
|
+
if (params.reasoningEffort && !/^grok-4\.3(?:$|[-.])/.test(resolvedModel)) {
|
|
1771
|
+
return createErrorResponse("grok_api_request", 1, "", corrId, new Error("reasoningEffort is currently supported only for xAI model grok-4.3"));
|
|
1772
|
+
}
|
|
1773
|
+
return {
|
|
1774
|
+
corrId,
|
|
1775
|
+
effectivePrompt,
|
|
1776
|
+
resolvedModel,
|
|
1777
|
+
instructions,
|
|
1778
|
+
input,
|
|
1779
|
+
stablePrefixHash: inputResolution.stablePrefixHash,
|
|
1780
|
+
stablePrefixTokens: inputResolution.stablePrefixTokens,
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
function usageFromXaiResult(result) {
|
|
1784
|
+
return {
|
|
1785
|
+
inputTokens: result.usage.inputTokens,
|
|
1786
|
+
outputTokens: result.usage.outputTokens,
|
|
1787
|
+
cacheReadTokens: result.usage.cacheReadTokens,
|
|
1788
|
+
costUsd: result.usage.costUsd,
|
|
1789
|
+
};
|
|
1790
|
+
}
|
|
1791
|
+
async function getExistingSessionForProvider(sessionManager, sessionId, provider) {
|
|
1792
|
+
if (!sessionId)
|
|
1793
|
+
return null;
|
|
1794
|
+
const existing = await sessionManager.getSession(sessionId);
|
|
1795
|
+
if (existing && existing.cli !== provider) {
|
|
1796
|
+
throw new Error(`Session ${sessionId} belongs to provider '${existing.cli}', not '${provider}'`);
|
|
1797
|
+
}
|
|
1798
|
+
return existing;
|
|
1799
|
+
}
|
|
1800
|
+
function asXaiApiError(error) {
|
|
1801
|
+
if (error instanceof XaiApiError)
|
|
1802
|
+
return error;
|
|
1803
|
+
const cause = error?.cause;
|
|
1804
|
+
return cause instanceof XaiApiError ? cause : null;
|
|
1805
|
+
}
|
|
1806
|
+
function buildGrokApiToolResponse(args) {
|
|
1807
|
+
let text = args.result.text;
|
|
1808
|
+
if (args.optimizeResponse) {
|
|
1809
|
+
const optimized = optimizeResponseText(text);
|
|
1810
|
+
logOptimizationTokens("response", args.corrId, text, optimized);
|
|
1811
|
+
text = optimized;
|
|
1812
|
+
}
|
|
1813
|
+
const response = {
|
|
1814
|
+
content: [{ type: "text", text }],
|
|
1815
|
+
structuredContent: {
|
|
1816
|
+
provider: "grok-api",
|
|
1817
|
+
cli: "grok-api",
|
|
1818
|
+
model: args.result.model || args.prep.resolvedModel,
|
|
1819
|
+
correlationId: args.corrId,
|
|
1820
|
+
sessionId: args.sessionId || null,
|
|
1821
|
+
responseId: args.result.responseId,
|
|
1822
|
+
previousResponseId: args.previousResponseId || null,
|
|
1823
|
+
stalePreviousResponseCleared: args.stalePreviousResponseCleared,
|
|
1824
|
+
status: args.result.status,
|
|
1825
|
+
httpStatus: args.result.httpStatus,
|
|
1826
|
+
durationMs: args.durationMs,
|
|
1827
|
+
...usageFromXaiResult(args.result),
|
|
1828
|
+
exitCode: 0,
|
|
1829
|
+
retryCount: 0,
|
|
1830
|
+
},
|
|
1831
|
+
};
|
|
1832
|
+
if (args.sessionId)
|
|
1833
|
+
response.sessionId = args.sessionId;
|
|
1834
|
+
return response;
|
|
1835
|
+
}
|
|
1836
|
+
async function resolveGrokApiSession(params, runtime) {
|
|
1837
|
+
if (params.sessionId) {
|
|
1838
|
+
const existing = await getExistingSessionForProvider(runtime.sessionManager, params.sessionId, "grok-api");
|
|
1839
|
+
const session = existing ??
|
|
1840
|
+
(await runtime.sessionManager.createSession("grok-api", "Grok API Session", params.sessionId));
|
|
1841
|
+
const previous = !params.createNewSession && typeof session.metadata?.xaiPreviousResponseId === "string"
|
|
1842
|
+
? session.metadata.xaiPreviousResponseId
|
|
1843
|
+
: undefined;
|
|
1844
|
+
return { sessionId: session.id, previousResponseId: previous };
|
|
1845
|
+
}
|
|
1846
|
+
if (!params.createNewSession) {
|
|
1847
|
+
const active = await runtime.sessionManager.getActiveSession("grok-api");
|
|
1848
|
+
if (active) {
|
|
1849
|
+
const previous = typeof active.metadata?.xaiPreviousResponseId === "string"
|
|
1850
|
+
? active.metadata.xaiPreviousResponseId
|
|
1851
|
+
: undefined;
|
|
1852
|
+
return { sessionId: active.id, previousResponseId: previous };
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
const session = await runtime.sessionManager.createSession("grok-api", "Grok API Session", `${GATEWAY_SESSION_PREFIX}${randomUUID()}`);
|
|
1856
|
+
return { sessionId: session.id };
|
|
1857
|
+
}
|
|
1858
|
+
export async function handleGrokApiRequest(deps, params) {
|
|
1859
|
+
const runtime = resolveHandlerRuntime(deps);
|
|
1860
|
+
const startTime = Date.now();
|
|
1861
|
+
const prep = prepareGrokApiRequest(params, runtime.providers);
|
|
1862
|
+
if ("content" in prep)
|
|
1863
|
+
return prep;
|
|
1864
|
+
const { corrId } = prep;
|
|
1865
|
+
const xaiConfig = runtime.providers.xai;
|
|
1866
|
+
let durationMs = 0;
|
|
1867
|
+
let wasSuccessful = false;
|
|
1868
|
+
try {
|
|
1869
|
+
await getExistingSessionForProvider(runtime.sessionManager, params.sessionId, "grok-api");
|
|
1870
|
+
}
|
|
1871
|
+
catch (err) {
|
|
1872
|
+
return createErrorResponse("grok_api_request", 1, "", corrId, err);
|
|
1873
|
+
}
|
|
1874
|
+
if (!xaiConfig) {
|
|
1875
|
+
return createErrorResponse("grok_api_request", 1, "", corrId, new Error("[providers.xai] is not configured"));
|
|
1876
|
+
}
|
|
1877
|
+
const apiKey = process.env[xaiConfig.apiKeyEnv]?.trim();
|
|
1878
|
+
if (!apiKey) {
|
|
1879
|
+
return createErrorResponse("grok_api_request", 1, "", corrId, new Error(`xAI API key env var ${xaiConfig.apiKeyEnv} is not set`));
|
|
1880
|
+
}
|
|
1881
|
+
safeFlightStart({
|
|
1882
|
+
correlationId: corrId,
|
|
1883
|
+
cli: "grok-api",
|
|
1884
|
+
model: prep.resolvedModel,
|
|
1885
|
+
prompt: prep.effectivePrompt,
|
|
1886
|
+
sessionId: params.sessionId,
|
|
1887
|
+
stablePrefixHash: prep.stablePrefixHash ?? undefined,
|
|
1888
|
+
stablePrefixTokens: prep.stablePrefixTokens ?? undefined,
|
|
1889
|
+
}, runtime);
|
|
1890
|
+
let sessionId;
|
|
1891
|
+
let previousResponseId;
|
|
1892
|
+
let stalePreviousResponseCleared = false;
|
|
1893
|
+
try {
|
|
1894
|
+
const session = await resolveGrokApiSession(params, runtime);
|
|
1895
|
+
sessionId = session.sessionId;
|
|
1896
|
+
previousResponseId = session.previousResponseId;
|
|
1897
|
+
const call = (prev) => createXaiResponse({
|
|
1898
|
+
baseUrl: xaiConfig.baseUrl,
|
|
1899
|
+
apiKey,
|
|
1900
|
+
model: prep.resolvedModel,
|
|
1901
|
+
input: prep.input,
|
|
1902
|
+
instructions: prep.instructions,
|
|
1903
|
+
previousResponseId: prev,
|
|
1904
|
+
maxOutputTokens: params.maxOutputTokens,
|
|
1905
|
+
temperature: params.temperature,
|
|
1906
|
+
topP: params.topP,
|
|
1907
|
+
reasoningEffort: params.reasoningEffort,
|
|
1908
|
+
timeoutMs: params.timeoutMs,
|
|
1909
|
+
}, runtime.logger);
|
|
1910
|
+
let result;
|
|
1911
|
+
try {
|
|
1912
|
+
result = await call(previousResponseId);
|
|
1913
|
+
}
|
|
1914
|
+
catch (error) {
|
|
1915
|
+
const xaiError = asXaiApiError(error);
|
|
1916
|
+
if (xaiError?.status === 404 && previousResponseId) {
|
|
1917
|
+
runtime.logger.warn(`[${corrId}] xAI previous_response_id was rejected; clearing stale session metadata and retrying fresh`);
|
|
1918
|
+
await runtime.sessionManager.updateSessionMetadata(sessionId, {
|
|
1919
|
+
xaiPreviousResponseId: null,
|
|
1920
|
+
xaiResponseCreatedAt: null,
|
|
1921
|
+
});
|
|
1922
|
+
stalePreviousResponseCleared = true;
|
|
1923
|
+
previousResponseId = undefined;
|
|
1924
|
+
result = await call(undefined);
|
|
1925
|
+
}
|
|
1926
|
+
else {
|
|
1927
|
+
throw error;
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
durationMs = Math.max(0, Date.now() - startTime);
|
|
1931
|
+
wasSuccessful = true;
|
|
1932
|
+
await runtime.sessionManager.updateSessionMetadata(sessionId, {
|
|
1933
|
+
xaiPreviousResponseId: result.responseId,
|
|
1934
|
+
xaiResponseCreatedAt: new Date().toISOString(),
|
|
1935
|
+
xaiModel: result.model || prep.resolvedModel,
|
|
1936
|
+
});
|
|
1937
|
+
await runtime.sessionManager.updateSessionUsage(sessionId);
|
|
1938
|
+
safeFlightComplete(corrId, {
|
|
1939
|
+
response: result.text,
|
|
1940
|
+
durationMs,
|
|
1941
|
+
retryCount: 0,
|
|
1942
|
+
circuitBreakerState: "closed",
|
|
1943
|
+
optimizationApplied: params.optimizePrompt || (params.optimizeResponse ?? false),
|
|
1944
|
+
exitCode: 0,
|
|
1945
|
+
status: "completed",
|
|
1946
|
+
...usageFromXaiResult(result),
|
|
1947
|
+
}, runtime);
|
|
1948
|
+
return buildGrokApiToolResponse({
|
|
1949
|
+
result,
|
|
1950
|
+
prep,
|
|
1951
|
+
corrId,
|
|
1952
|
+
durationMs,
|
|
1953
|
+
sessionId,
|
|
1954
|
+
previousResponseId,
|
|
1955
|
+
stalePreviousResponseCleared,
|
|
1956
|
+
optimizeResponse: params.optimizeResponse ?? false,
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
catch (error) {
|
|
1960
|
+
durationMs = Math.max(0, Date.now() - startTime);
|
|
1961
|
+
const err = error;
|
|
1962
|
+
const xaiError = asXaiApiError(error);
|
|
1963
|
+
runtime.logger.error(`[${corrId}] grok_api_request failed`, err.message);
|
|
1964
|
+
safeFlightComplete(corrId, {
|
|
1965
|
+
response: xaiError?.responseText ?? "",
|
|
1966
|
+
durationMs,
|
|
1967
|
+
retryCount: 0,
|
|
1968
|
+
circuitBreakerState: "closed",
|
|
1969
|
+
optimizationApplied: false,
|
|
1970
|
+
exitCode: 1,
|
|
1971
|
+
errorMessage: err.message,
|
|
1972
|
+
status: "failed",
|
|
1973
|
+
}, runtime);
|
|
1974
|
+
return createErrorResponse("grok_api_request", 1, "", corrId, err);
|
|
1975
|
+
}
|
|
1976
|
+
finally {
|
|
1977
|
+
runtime.performanceMetrics.recordRequest("grok-api", durationMs || Math.max(0, Date.now() - startTime), wasSuccessful);
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1679
1980
|
function maybeBuildCacheTtlWarning(args) {
|
|
1680
1981
|
if (args.cli !== "claude")
|
|
1681
1982
|
return null;
|
|
@@ -1762,9 +2063,12 @@ export async function handleGeminiRequest(deps, params) {
|
|
|
1762
2063
|
resumeLatest: params.resumeLatest,
|
|
1763
2064
|
createNewSession: params.createNewSession,
|
|
1764
2065
|
});
|
|
1765
|
-
args.push(...sessionPlan.args);
|
|
1766
2066
|
const userProvidedSession = sessionPlan.resumed;
|
|
1767
2067
|
const effectiveSessionIdHint = sessionPlan.resumed ? params.sessionId : undefined;
|
|
2068
|
+
if (effectiveSessionIdHint) {
|
|
2069
|
+
await getExistingSessionForProvider(deps.sessionManager, effectiveSessionIdHint, "gemini");
|
|
2070
|
+
}
|
|
2071
|
+
args.push(...sessionPlan.args);
|
|
1768
2072
|
let worktreeResolution = {};
|
|
1769
2073
|
try {
|
|
1770
2074
|
worktreeResolution = await resolveWorktreeForRequest(params.worktree, effectiveSessionIdHint, runtime);
|
|
@@ -1887,11 +2191,11 @@ export async function handleGeminiRequestAsync(deps, params) {
|
|
|
1887
2191
|
resumeLatest: params.resumeLatest,
|
|
1888
2192
|
createNewSession: params.createNewSession,
|
|
1889
2193
|
});
|
|
1890
|
-
args.push(...sessionPlan.args);
|
|
1891
2194
|
let effectiveSessionId = sessionPlan.resumed ? params.sessionId : undefined;
|
|
2195
|
+
const existingSession = await getExistingSessionForProvider(deps.sessionManager, effectiveSessionId, "gemini");
|
|
2196
|
+
args.push(...sessionPlan.args);
|
|
1892
2197
|
if (effectiveSessionId) {
|
|
1893
|
-
|
|
1894
|
-
if (!existing) {
|
|
2198
|
+
if (!existingSession) {
|
|
1895
2199
|
try {
|
|
1896
2200
|
await deps.sessionManager.createSession("gemini", "Gemini Session", effectiveSessionId);
|
|
1897
2201
|
}
|
|
@@ -2012,6 +2316,9 @@ export async function handleGrokRequest(deps, params) {
|
|
|
2012
2316
|
resumeLatest: params.resumeLatest,
|
|
2013
2317
|
createNewSession: params.createNewSession,
|
|
2014
2318
|
});
|
|
2319
|
+
if (sessionResult.userProvidedSession) {
|
|
2320
|
+
await getExistingSessionForProvider(deps.sessionManager, sessionResult.effectiveSessionId, "grok");
|
|
2321
|
+
}
|
|
2015
2322
|
args.push(...sessionResult.resumeArgs);
|
|
2016
2323
|
let worktreeResolution = {};
|
|
2017
2324
|
try {
|
|
@@ -2158,6 +2465,9 @@ export async function handleGrokRequestAsync(deps, params) {
|
|
|
2158
2465
|
resumeLatest: params.resumeLatest,
|
|
2159
2466
|
createNewSession: params.createNewSession,
|
|
2160
2467
|
});
|
|
2468
|
+
if (sessionResult.userProvidedSession) {
|
|
2469
|
+
await getExistingSessionForProvider(deps.sessionManager, sessionResult.effectiveSessionId, "grok");
|
|
2470
|
+
}
|
|
2161
2471
|
args.push(...sessionResult.resumeArgs);
|
|
2162
2472
|
let effectiveSessionId = sessionResult.effectiveSessionId;
|
|
2163
2473
|
if (sessionResult.userProvidedSession && effectiveSessionId) {
|
|
@@ -2262,6 +2572,9 @@ export async function handleMistralRequest(deps, params) {
|
|
|
2262
2572
|
resumeLatest: params.resumeLatest,
|
|
2263
2573
|
createNewSession: params.createNewSession,
|
|
2264
2574
|
});
|
|
2575
|
+
if (sessionResult.userProvidedSession) {
|
|
2576
|
+
await getExistingSessionForProvider(deps.sessionManager, sessionResult.effectiveSessionId, "mistral");
|
|
2577
|
+
}
|
|
2265
2578
|
args.push(...sessionResult.resumeArgs);
|
|
2266
2579
|
let worktreeResolution = {};
|
|
2267
2580
|
try {
|
|
@@ -2397,11 +2710,11 @@ export async function handleMistralRequestAsync(deps, params) {
|
|
|
2397
2710
|
resumeLatest: params.resumeLatest,
|
|
2398
2711
|
createNewSession: params.createNewSession,
|
|
2399
2712
|
});
|
|
2400
|
-
args.push(...sessionResult.resumeArgs);
|
|
2401
2713
|
let effectiveSessionId = sessionResult.effectiveSessionId;
|
|
2714
|
+
const existingSession = await getExistingSessionForProvider(deps.sessionManager, sessionResult.userProvidedSession ? effectiveSessionId : undefined, "mistral");
|
|
2715
|
+
args.push(...sessionResult.resumeArgs);
|
|
2402
2716
|
if (sessionResult.userProvidedSession && effectiveSessionId) {
|
|
2403
|
-
|
|
2404
|
-
if (!existing) {
|
|
2717
|
+
if (!existingSession) {
|
|
2405
2718
|
try {
|
|
2406
2719
|
await deps.sessionManager.createSession("mistral", "Mistral Session", effectiveSessionId);
|
|
2407
2720
|
}
|
|
@@ -2458,6 +2771,12 @@ export async function handleMistralRequestAsync(deps, params) {
|
|
|
2458
2771
|
}
|
|
2459
2772
|
export async function handleCodexRequestAsync(deps, params) {
|
|
2460
2773
|
const runtime = resolveHandlerRuntime(deps);
|
|
2774
|
+
try {
|
|
2775
|
+
await getExistingSessionForProvider(deps.sessionManager, params.sessionId, "codex");
|
|
2776
|
+
}
|
|
2777
|
+
catch (err) {
|
|
2778
|
+
return createErrorResponse("codex_request_async", 1, "", params.correlationId, err);
|
|
2779
|
+
}
|
|
2461
2780
|
const prep = prepareCodexRequest({
|
|
2462
2781
|
prompt: params.prompt,
|
|
2463
2782
|
promptParts: params.promptParts,
|
|
@@ -2572,13 +2891,89 @@ export async function handleCodexRequestAsync(deps, params) {
|
|
|
2572
2891
|
}
|
|
2573
2892
|
export function createGatewayServer(deps = {}) {
|
|
2574
2893
|
const runtime = resolveGatewayServerRuntime(deps, { isolateState: true });
|
|
2575
|
-
const { sessionManager, asyncJobManager, approvalManager, performanceMetrics, logger, persistence, flightRecorder, cacheAwareness, } = runtime;
|
|
2894
|
+
const { sessionManager, asyncJobManager, approvalManager, performanceMetrics, logger, persistence, flightRecorder, cacheAwareness, providers, } = runtime;
|
|
2576
2895
|
void flightRecorder;
|
|
2577
2896
|
void cacheAwareness;
|
|
2897
|
+
const grokApiToolsEnabled = shouldRegisterGrokApiTools(providers);
|
|
2578
2898
|
const asyncJobsEnabled = persistence.backend !== "none" && persistence.asyncJobsEnabled && asyncJobManager.hasStore();
|
|
2579
|
-
const server = newGatewayMcpServer(asyncJobsEnabled);
|
|
2899
|
+
const server = newGatewayMcpServer(asyncJobsEnabled, grokApiToolsEnabled);
|
|
2580
2900
|
registerBaseResources(server, runtime);
|
|
2581
2901
|
registerValidationTools(server, { asyncJobManager });
|
|
2902
|
+
if (grokApiToolsEnabled) {
|
|
2903
|
+
server.tool("grok_api_request", "Run an xAI Grok API request synchronously through the Responses API. Requires exactly one of prompt or promptParts. Registered only when [providers.xai] is configured and its API-key env var is present.", {
|
|
2904
|
+
prompt: z
|
|
2905
|
+
.string()
|
|
2906
|
+
.min(1, "Prompt cannot be empty")
|
|
2907
|
+
.max(100000, "Prompt too long (max 100k chars)")
|
|
2908
|
+
.optional()
|
|
2909
|
+
.describe("Prompt text for xAI Grok API (mutually exclusive with promptParts)"),
|
|
2910
|
+
promptParts: PromptPartsSchema.optional().describe("Cache-aware structured prompt: { system?, tools?, context?, task }. Mutually exclusive with prompt. The stable prefix hash is logged for cache_state aggregates; xAI does not receive cache_control hints."),
|
|
2911
|
+
model: z
|
|
2912
|
+
.string()
|
|
2913
|
+
.min(1)
|
|
2914
|
+
.optional()
|
|
2915
|
+
.describe("xAI model id; defaults to [providers.xai].default_model"),
|
|
2916
|
+
sessionId: z
|
|
2917
|
+
.string()
|
|
2918
|
+
.optional()
|
|
2919
|
+
.describe("Gateway grok-api session to continue. The gateway stores xAI previous_response_id in session metadata."),
|
|
2920
|
+
createNewSession: z
|
|
2921
|
+
.boolean()
|
|
2922
|
+
.default(false)
|
|
2923
|
+
.describe("Start a fresh xAI response chain. With sessionId, ignores any stored previous_response_id for this request."),
|
|
2924
|
+
correlationId: z.string().optional().describe("Request trace ID (auto if omitted)"),
|
|
2925
|
+
optimizePrompt: z.boolean().default(false).describe("Optimize prompt before execution"),
|
|
2926
|
+
optimizeResponse: z.boolean().default(false).describe("Optimize response output"),
|
|
2927
|
+
maxOutputTokens: MAX_TOKENS_SCHEMA.optional().describe("xAI Responses API max_output_tokens. Bounded to safe integers <= 100000000."),
|
|
2928
|
+
temperature: z
|
|
2929
|
+
.number()
|
|
2930
|
+
.finite()
|
|
2931
|
+
.min(0)
|
|
2932
|
+
.max(2)
|
|
2933
|
+
.optional()
|
|
2934
|
+
.describe("Sampling temperature passed to xAI Responses API"),
|
|
2935
|
+
topP: z
|
|
2936
|
+
.number()
|
|
2937
|
+
.finite()
|
|
2938
|
+
.min(0)
|
|
2939
|
+
.max(1)
|
|
2940
|
+
.optional()
|
|
2941
|
+
.describe("Nucleus sampling top_p passed to xAI Responses API"),
|
|
2942
|
+
reasoningEffort: z
|
|
2943
|
+
.enum(["none", "low", "medium", "high"])
|
|
2944
|
+
.optional()
|
|
2945
|
+
.describe("xAI Responses API reasoning.effort"),
|
|
2946
|
+
timeoutMs: z
|
|
2947
|
+
.number()
|
|
2948
|
+
.int()
|
|
2949
|
+
.min(30_000)
|
|
2950
|
+
.max(3_600_000)
|
|
2951
|
+
.optional()
|
|
2952
|
+
.describe("HTTP request timeout in ms (min 30s, max 1h, default 10m)"),
|
|
2953
|
+
}, {
|
|
2954
|
+
title: "Grok API request",
|
|
2955
|
+
readOnlyHint: false,
|
|
2956
|
+
destructiveHint: false,
|
|
2957
|
+
idempotentHint: false,
|
|
2958
|
+
openWorldHint: true,
|
|
2959
|
+
}, async ({ prompt, promptParts, model, sessionId, createNewSession, correlationId, optimizePrompt, optimizeResponse, maxOutputTokens, temperature, topP, reasoningEffort, timeoutMs, }) => {
|
|
2960
|
+
return handleGrokApiRequest({ sessionManager, logger, runtime }, {
|
|
2961
|
+
prompt,
|
|
2962
|
+
promptParts,
|
|
2963
|
+
model,
|
|
2964
|
+
sessionId,
|
|
2965
|
+
createNewSession,
|
|
2966
|
+
correlationId,
|
|
2967
|
+
optimizePrompt,
|
|
2968
|
+
optimizeResponse,
|
|
2969
|
+
maxOutputTokens,
|
|
2970
|
+
temperature,
|
|
2971
|
+
topP,
|
|
2972
|
+
reasoningEffort,
|
|
2973
|
+
timeoutMs,
|
|
2974
|
+
});
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2582
2977
|
server.tool("claude_request", "Run a Claude Code CLI request synchronously (when async jobs are enabled, auto-defers to a pollable job past the sync deadline; otherwise runs to completion). Requires exactly one of prompt or promptParts.", {
|
|
2583
2978
|
prompt: z
|
|
2584
2979
|
.string()
|
|
@@ -2783,6 +3178,12 @@ export function createGatewayServer(deps = {}) {
|
|
|
2783
3178
|
if (!useContinue && effectiveSessionId && activeSession?.id === effectiveSessionId) {
|
|
2784
3179
|
useContinue = true;
|
|
2785
3180
|
}
|
|
3181
|
+
try {
|
|
3182
|
+
await getExistingSessionForProvider(sessionManager, effectiveSessionId, "claude");
|
|
3183
|
+
}
|
|
3184
|
+
catch (err) {
|
|
3185
|
+
return createErrorResponse("claude_request", 1, "", corrId, err);
|
|
3186
|
+
}
|
|
2786
3187
|
const ttlWarning = maybeBuildCacheTtlWarning({
|
|
2787
3188
|
runtime,
|
|
2788
3189
|
sessionId: effectiveSessionId,
|
|
@@ -3068,6 +3469,12 @@ export function createGatewayServer(deps = {}) {
|
|
|
3068
3469
|
const { corrId, args } = prep;
|
|
3069
3470
|
let durationMs = 0;
|
|
3070
3471
|
let wasSuccessful = false;
|
|
3472
|
+
try {
|
|
3473
|
+
await getExistingSessionForProvider(sessionManager, sessionId, "codex");
|
|
3474
|
+
}
|
|
3475
|
+
catch (err) {
|
|
3476
|
+
return createErrorResponse("codex_request", 1, "", corrId, err);
|
|
3477
|
+
}
|
|
3071
3478
|
safeFlightStart({
|
|
3072
3479
|
correlationId: corrId,
|
|
3073
3480
|
cli: "codex",
|
|
@@ -3220,6 +3627,12 @@ export function createGatewayServer(deps = {}) {
|
|
|
3220
3627
|
if (!sessionId && !forkLast) {
|
|
3221
3628
|
return createErrorResponse("codex_fork_session", 1, "", corrId, new Error("one of sessionId or forkLast is required"));
|
|
3222
3629
|
}
|
|
3630
|
+
try {
|
|
3631
|
+
await getExistingSessionForProvider(sessionManager, sessionId, "codex");
|
|
3632
|
+
}
|
|
3633
|
+
catch (err) {
|
|
3634
|
+
return createErrorResponse("codex_fork_session", 1, "", corrId, err);
|
|
3635
|
+
}
|
|
3223
3636
|
let forkArgs;
|
|
3224
3637
|
try {
|
|
3225
3638
|
forkArgs = prepareCodexForkRequest({ prompt, sessionId, forkLast }).args;
|
|
@@ -3916,6 +4329,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
3916
4329
|
if (!useContinue && effectiveSessionId && activeSession?.id === effectiveSessionId) {
|
|
3917
4330
|
useContinue = true;
|
|
3918
4331
|
}
|
|
4332
|
+
const existingSession = await getExistingSessionForProvider(sessionManager, effectiveSessionId, "claude");
|
|
3919
4333
|
if (useContinue) {
|
|
3920
4334
|
args.push("--continue");
|
|
3921
4335
|
}
|
|
@@ -3924,7 +4338,6 @@ export function createGatewayServer(deps = {}) {
|
|
|
3924
4338
|
await sessionManager.updateSessionUsage(effectiveSessionId);
|
|
3925
4339
|
}
|
|
3926
4340
|
if (effectiveSessionId) {
|
|
3927
|
-
const existingSession = await sessionManager.getSession(effectiveSessionId);
|
|
3928
4341
|
if (!existingSession) {
|
|
3929
4342
|
await sessionManager.createSession("claude", "Claude Session", effectiveSessionId);
|
|
3930
4343
|
}
|
|
@@ -4777,11 +5190,33 @@ export function createGatewayServer(deps = {}) {
|
|
|
4777
5190
|
? null
|
|
4778
5191
|
: "Async job persistence is disabled (backend = 'none'). *_request_async tools are NOT registered on this gateway. Set [persistence].backend = 'sqlite' (or 'memory' + acknowledgeEphemeral = true) to enable them.",
|
|
4779
5192
|
};
|
|
5193
|
+
const outboundProviders = {
|
|
5194
|
+
xai: providers.xai
|
|
5195
|
+
? {
|
|
5196
|
+
configured: true,
|
|
5197
|
+
enabled: isXaiProviderEnabled(providers),
|
|
5198
|
+
apiKeyEnv: providers.xai.apiKeyEnv,
|
|
5199
|
+
apiKeyPresent: isXaiProviderEnabled(providers),
|
|
5200
|
+
baseUrl: providers.xai.baseUrl,
|
|
5201
|
+
defaultModel: providers.xai.defaultModel,
|
|
5202
|
+
mode: isXaiProviderEnabled(providers) ? "sync" : "configured-missing-key",
|
|
5203
|
+
}
|
|
5204
|
+
: {
|
|
5205
|
+
configured: false,
|
|
5206
|
+
enabled: false,
|
|
5207
|
+
apiKeyEnv: null,
|
|
5208
|
+
apiKeyPresent: false,
|
|
5209
|
+
baseUrl: null,
|
|
5210
|
+
defaultModel: null,
|
|
5211
|
+
mode: "disabled",
|
|
5212
|
+
},
|
|
5213
|
+
sources: providers.sources,
|
|
5214
|
+
};
|
|
4780
5215
|
return {
|
|
4781
5216
|
content: [
|
|
4782
5217
|
{
|
|
4783
5218
|
type: "text",
|
|
4784
|
-
text: JSON.stringify({ success: true, ...health, persistence: persistenceBlock }, null, 2),
|
|
5219
|
+
text: JSON.stringify({ success: true, ...health, persistence: persistenceBlock, outboundProviders }, null, 2),
|
|
4785
5220
|
},
|
|
4786
5221
|
],
|
|
4787
5222
|
};
|
|
@@ -4850,7 +5285,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
4850
5285
|
});
|
|
4851
5286
|
server.tool("upstream_contracts", "Return the gateway's declared provider CLI contracts; with probeInstalled true, diff against installed --help surfaces to detect flag drift.", {
|
|
4852
5287
|
cli: z
|
|
4853
|
-
.preprocess(value => (value === "" || value === null ? undefined : value),
|
|
5288
|
+
.preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
|
|
4854
5289
|
.describe("CLI filter (claude|codex|gemini|grok|mistral)"),
|
|
4855
5290
|
probeInstalled: z
|
|
4856
5291
|
.boolean()
|
|
@@ -4866,6 +5301,133 @@ export function createGatewayServer(deps = {}) {
|
|
|
4866
5301
|
const report = buildUpstreamContractReport({ cli, probeInstalled });
|
|
4867
5302
|
return { content: [{ type: "text", text: JSON.stringify(report, null, 2) }] };
|
|
4868
5303
|
});
|
|
5304
|
+
server.tool("provider_subcommands_list", "Return a compact, filterable read-only catalog of declared provider CLI subcommands without flags or raw help.", {
|
|
5305
|
+
provider: z
|
|
5306
|
+
.preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
|
|
5307
|
+
.describe("Optional provider filter (claude|codex|gemini|grok|mistral)"),
|
|
5308
|
+
tier: z
|
|
5309
|
+
.enum(["catalog", "inspect", "execute_candidate", "diagnostic"])
|
|
5310
|
+
.optional()
|
|
5311
|
+
.describe("Optional subcommand tier filter"),
|
|
5312
|
+
risk: z
|
|
5313
|
+
.enum([
|
|
5314
|
+
"read_only",
|
|
5315
|
+
"writes_local_config",
|
|
5316
|
+
"auth",
|
|
5317
|
+
"network",
|
|
5318
|
+
"starts_server",
|
|
5319
|
+
"updates_binary",
|
|
5320
|
+
"destructive",
|
|
5321
|
+
"executes_agent",
|
|
5322
|
+
])
|
|
5323
|
+
.optional()
|
|
5324
|
+
.describe("Optional risk classification filter"),
|
|
5325
|
+
exposure: z
|
|
5326
|
+
.enum(["tracked_only", "mcp_readonly", "mcp_requires_approval", "not_exposed"])
|
|
5327
|
+
.optional()
|
|
5328
|
+
.describe("Optional MCP exposure filter"),
|
|
5329
|
+
commandPathPrefix: z
|
|
5330
|
+
.array(z.string().min(1))
|
|
5331
|
+
.optional()
|
|
5332
|
+
.describe("Optional command path prefix filter, e.g. ['agent']"),
|
|
5333
|
+
}, {
|
|
5334
|
+
title: "Provider subcommands catalog",
|
|
5335
|
+
readOnlyHint: true,
|
|
5336
|
+
destructiveHint: false,
|
|
5337
|
+
idempotentHint: true,
|
|
5338
|
+
openWorldHint: false,
|
|
5339
|
+
}, async ({ provider, tier, risk, exposure, commandPathPrefix }) => {
|
|
5340
|
+
const catalog = buildProviderSubcommandsCompactCatalog({
|
|
5341
|
+
provider,
|
|
5342
|
+
tier,
|
|
5343
|
+
risk,
|
|
5344
|
+
exposure,
|
|
5345
|
+
commandPathPrefix,
|
|
5346
|
+
});
|
|
5347
|
+
return {
|
|
5348
|
+
content: [
|
|
5349
|
+
{
|
|
5350
|
+
type: "text",
|
|
5351
|
+
text: JSON.stringify({ ...catalog, total: catalog.rows.length }),
|
|
5352
|
+
},
|
|
5353
|
+
],
|
|
5354
|
+
};
|
|
5355
|
+
});
|
|
5356
|
+
server.tool("provider_subcommand_contract", "Return the detailed read-only contract for exactly one declared provider CLI subcommand.", {
|
|
5357
|
+
provider: CLI_TYPE_ENUM.describe("Provider (claude|codex|gemini|grok|mistral)"),
|
|
5358
|
+
commandPath: z.array(z.string().min(1)).min(1).describe("Command path segments"),
|
|
5359
|
+
}, {
|
|
5360
|
+
title: "Provider subcommand contract",
|
|
5361
|
+
readOnlyHint: true,
|
|
5362
|
+
destructiveHint: false,
|
|
5363
|
+
idempotentHint: true,
|
|
5364
|
+
openWorldHint: false,
|
|
5365
|
+
}, async ({ provider, commandPath }) => {
|
|
5366
|
+
const contract = getCliSubcommandContract(provider, commandPath);
|
|
5367
|
+
const payload = contract
|
|
5368
|
+
? {
|
|
5369
|
+
schemaVersion: "provider-subcommand-contract.v1",
|
|
5370
|
+
contract: serializeCliSubcommandContract(provider, contract),
|
|
5371
|
+
}
|
|
5372
|
+
: {
|
|
5373
|
+
schemaVersion: "provider-subcommand-contract.v1",
|
|
5374
|
+
error: `No declared ${provider} subcommand contract for ${commandPath.join(" ")}`,
|
|
5375
|
+
};
|
|
5376
|
+
return { content: [{ type: "text", text: JSON.stringify(payload, null, 2) }] };
|
|
5377
|
+
});
|
|
5378
|
+
server.tool("provider_subcommand_drift", "Probe declared provider subcommand --help surfaces and return compact drift rows without raw help output.", {
|
|
5379
|
+
provider: z
|
|
5380
|
+
.preprocess(value => (value === "" || value === null ? undefined : value), CLI_TYPE_ENUM.optional())
|
|
5381
|
+
.describe("Optional provider filter (claude|codex|gemini|grok|mistral)"),
|
|
5382
|
+
includeClean: z
|
|
5383
|
+
.boolean()
|
|
5384
|
+
.default(false)
|
|
5385
|
+
.describe("When false, return only unavailable or drifted command paths"),
|
|
5386
|
+
}, {
|
|
5387
|
+
title: "Provider subcommand drift",
|
|
5388
|
+
readOnlyHint: true,
|
|
5389
|
+
destructiveHint: false,
|
|
5390
|
+
idempotentHint: true,
|
|
5391
|
+
openWorldHint: false,
|
|
5392
|
+
}, async ({ provider, includeClean }) => {
|
|
5393
|
+
const providers = provider ? [provider] : CLI_TYPES;
|
|
5394
|
+
const rows = providers.flatMap(cli => {
|
|
5395
|
+
const probe = probeInstalledCliContract(cli);
|
|
5396
|
+
return Object.values(probe.subcommands).flatMap(sub => {
|
|
5397
|
+
const drifted = !sub.available || sub.extraFlags.length > 0 || sub.missingFlags.length > 0;
|
|
5398
|
+
if (!includeClean && !drifted)
|
|
5399
|
+
return [];
|
|
5400
|
+
return [
|
|
5401
|
+
{
|
|
5402
|
+
provider: cli,
|
|
5403
|
+
commandPath: sub.commandPath,
|
|
5404
|
+
driftStatus: drifted ? "drift" : "clean",
|
|
5405
|
+
available: sub.available,
|
|
5406
|
+
extraVsContract: sub.extraFlags,
|
|
5407
|
+
missingFromBinary: sub.missingFlags,
|
|
5408
|
+
helpHash: sub.helpHash ?? null,
|
|
5409
|
+
risk: sub.risk,
|
|
5410
|
+
exposure: sub.exposure,
|
|
5411
|
+
tier: sub.tier,
|
|
5412
|
+
summary: sub.summary,
|
|
5413
|
+
warnings: sub.warnings,
|
|
5414
|
+
},
|
|
5415
|
+
];
|
|
5416
|
+
});
|
|
5417
|
+
});
|
|
5418
|
+
return {
|
|
5419
|
+
content: [
|
|
5420
|
+
{
|
|
5421
|
+
type: "text",
|
|
5422
|
+
text: JSON.stringify({
|
|
5423
|
+
schemaVersion: "provider-subcommand-drift.v1",
|
|
5424
|
+
total: rows.length,
|
|
5425
|
+
rows,
|
|
5426
|
+
}),
|
|
5427
|
+
},
|
|
5428
|
+
],
|
|
5429
|
+
};
|
|
5430
|
+
});
|
|
4869
5431
|
server.tool("cli_upgrade", "Plan (dryRun, default true) or execute an upgrade for one provider CLI using its native update mechanism.", {
|
|
4870
5432
|
cli: z.enum(["claude", "codex", "gemini", "grok", "mistral"]).describe("CLI to upgrade"),
|
|
4871
5433
|
target: z
|
|
@@ -4921,8 +5483,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
4921
5483
|
};
|
|
4922
5484
|
}
|
|
4923
5485
|
});
|
|
4924
|
-
server.tool("session_create", "Create a gateway session record for a provider
|
|
4925
|
-
cli: SESSION_PROVIDER_ENUM.describe("
|
|
5486
|
+
server.tool("session_create", "Create a gateway session record for a provider. NOTE: this is gateway bookkeeping (gw-* ID), not a provider-native session — Codex resume needs a real Codex UUID.", {
|
|
5487
|
+
cli: SESSION_PROVIDER_ENUM.describe("Provider type (claude|codex|gemini|grok|mistral|grok-api)"),
|
|
4926
5488
|
description: z.string().optional().describe("Session description"),
|
|
4927
5489
|
setAsActive: z.boolean().default(true).describe("Set as active session"),
|
|
4928
5490
|
}, {
|
|
@@ -4960,8 +5522,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
4960
5522
|
return createErrorResponse("session_create", 1, "", undefined, error);
|
|
4961
5523
|
}
|
|
4962
5524
|
});
|
|
4963
|
-
server.tool("session_list", "List gateway session records and the active session per
|
|
4964
|
-
cli: SESSION_PROVIDER_ENUM.optional().describe("
|
|
5525
|
+
server.tool("session_list", "List gateway session records and the active session per provider, optionally filtered by provider.", {
|
|
5526
|
+
cli: SESSION_PROVIDER_ENUM.optional().describe("Provider filter (claude|codex|gemini|grok|mistral|grok-api)"),
|
|
4965
5527
|
}, {
|
|
4966
5528
|
title: "List sessions",
|
|
4967
5529
|
readOnlyHint: true,
|
|
@@ -4971,13 +5533,10 @@ export function createGatewayServer(deps = {}) {
|
|
|
4971
5533
|
}, async ({ cli }) => {
|
|
4972
5534
|
try {
|
|
4973
5535
|
const sessions = await sessionManager.listSessions(cli);
|
|
4974
|
-
const activeSessions =
|
|
4975
|
-
|
|
4976
|
-
|
|
4977
|
-
|
|
4978
|
-
grok: await sessionManager.getActiveSession("grok"),
|
|
4979
|
-
mistral: await sessionManager.getActiveSession("mistral"),
|
|
4980
|
-
};
|
|
5536
|
+
const activeSessions = Object.fromEntries(await Promise.all(SESSION_PROVIDER_VALUES.map(async (provider) => [
|
|
5537
|
+
provider,
|
|
5538
|
+
await sessionManager.getActiveSession(provider),
|
|
5539
|
+
])));
|
|
4981
5540
|
const sessionList = sessions.map(s => ({
|
|
4982
5541
|
id: s.id,
|
|
4983
5542
|
cli: s.cli,
|
|
@@ -4993,13 +5552,10 @@ export function createGatewayServer(deps = {}) {
|
|
|
4993
5552
|
text: JSON.stringify({
|
|
4994
5553
|
total: sessionList.length,
|
|
4995
5554
|
sessions: sessionList,
|
|
4996
|
-
activeSessions:
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
grok: activeSessions.grok?.id || null,
|
|
5001
|
-
mistral: activeSessions.mistral?.id || null,
|
|
5002
|
-
},
|
|
5555
|
+
activeSessions: Object.fromEntries(SESSION_PROVIDER_VALUES.map(provider => [
|
|
5556
|
+
provider,
|
|
5557
|
+
activeSessions[provider]?.id || null,
|
|
5558
|
+
])),
|
|
5003
5559
|
}, null, 2),
|
|
5004
5560
|
},
|
|
5005
5561
|
],
|
|
@@ -5009,8 +5565,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
5009
5565
|
return createErrorResponse("session_list", 1, "", undefined, error);
|
|
5010
5566
|
}
|
|
5011
5567
|
});
|
|
5012
|
-
server.tool("session_set_active", "Set or clear the active session for a
|
|
5013
|
-
cli: SESSION_PROVIDER_ENUM.describe("
|
|
5568
|
+
server.tool("session_set_active", "Set or clear the active session for a provider; the active session is used when a request omits sessionId.", {
|
|
5569
|
+
cli: SESSION_PROVIDER_ENUM.describe("Provider type (claude|codex|gemini|grok|mistral|grok-api)"),
|
|
5014
5570
|
sessionId: z.string().nullable().describe("Session ID (null to clear)"),
|
|
5015
5571
|
}, {
|
|
5016
5572
|
title: "Set active session",
|
|
@@ -5028,7 +5584,7 @@ export function createGatewayServer(deps = {}) {
|
|
|
5028
5584
|
type: "text",
|
|
5029
5585
|
text: JSON.stringify({
|
|
5030
5586
|
success: false,
|
|
5031
|
-
error: "Session not found or does not belong to the specified
|
|
5587
|
+
error: "Session not found or does not belong to the specified provider",
|
|
5032
5588
|
}, null, 2),
|
|
5033
5589
|
},
|
|
5034
5590
|
],
|
|
@@ -5169,8 +5725,8 @@ export function createGatewayServer(deps = {}) {
|
|
|
5169
5725
|
return createErrorResponse("session_get", 1, "", undefined, error);
|
|
5170
5726
|
}
|
|
5171
5727
|
});
|
|
5172
|
-
server.tool("session_clear_all", "Delete all gateway session records, optionally scoped to one
|
|
5173
|
-
cli: SESSION_PROVIDER_ENUM.optional().describe("
|
|
5728
|
+
server.tool("session_clear_all", "Delete all gateway session records, optionally scoped to one provider.", {
|
|
5729
|
+
cli: SESSION_PROVIDER_ENUM.optional().describe("Provider filter (claude|codex|gemini|grok|mistral|grok-api)"),
|
|
5174
5730
|
}, {
|
|
5175
5731
|
title: "Clear sessions",
|
|
5176
5732
|
readOnlyHint: false,
|
|
@@ -5326,9 +5882,7 @@ async function main() {
|
|
|
5326
5882
|
if (args[0] === "contracts") {
|
|
5327
5883
|
if (args.includes("--json")) {
|
|
5328
5884
|
const cliArg = args.find(arg => arg.startsWith("--cli="))?.split("=")[1];
|
|
5329
|
-
const cli =
|
|
5330
|
-
? cliArg
|
|
5331
|
-
: undefined;
|
|
5885
|
+
const cli = CLI_TYPES.includes(cliArg) ? cliArg : undefined;
|
|
5332
5886
|
if (cliArg && !cli) {
|
|
5333
5887
|
process.stderr.write(`Unsupported --cli value: ${cliArg}\n`);
|
|
5334
5888
|
process.exit(2);
|