llm-cli-gateway 2.13.1 → 2.14.0-rc.1
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 +140 -0
- package/README.md +53 -26
- package/dist/acp/client.d.ts +29 -1
- package/dist/acp/client.js +78 -4
- package/dist/acp/errors.d.ts +9 -1
- package/dist/acp/errors.js +19 -0
- package/dist/acp/event-normalizer.d.ts +12 -0
- package/dist/acp/event-normalizer.js +16 -0
- package/dist/acp/flight-redaction.d.ts +3 -0
- package/dist/acp/flight-redaction.js +3 -0
- package/dist/acp/permission-bridge.js +11 -5
- package/dist/acp/process-manager.d.ts +2 -1
- package/dist/acp/process-manager.js +43 -4
- package/dist/acp/provider-registry.js +19 -11
- package/dist/acp/runtime.d.ts +8 -0
- package/dist/acp/runtime.js +47 -4
- package/dist/acp/types.d.ts +3083 -55
- package/dist/acp/types.js +242 -5
- package/dist/async-job-manager.d.ts +2 -0
- package/dist/async-job-manager.js +11 -0
- package/dist/codex-json-parser.d.ts +1 -0
- package/dist/codex-json-parser.js +6 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +105 -19
- package/dist/connector-setup.d.ts +39 -0
- package/dist/connector-setup.js +86 -0
- package/dist/doctor.d.ts +39 -1
- package/dist/doctor.js +182 -3
- package/dist/flight-recorder.d.ts +2 -0
- package/dist/flight-recorder.js +20 -0
- package/dist/gemini-json-parser.d.ts +2 -0
- package/dist/gemini-json-parser.js +45 -8
- package/dist/grok-json-parser.d.ts +14 -0
- package/dist/grok-json-parser.js +156 -0
- package/dist/http-transport.js +27 -3
- package/dist/index.d.ts +48 -2
- package/dist/index.js +633 -144
- package/dist/job-store.d.ts +45 -7
- package/dist/job-store.js +138 -17
- package/dist/model-registry.d.ts +1 -0
- package/dist/model-registry.js +46 -0
- package/dist/oauth.js +17 -16
- package/dist/postgres-job-store-worker.d.ts +1 -0
- package/dist/postgres-job-store-worker.js +327 -0
- package/dist/pricing.d.ts +3 -2
- package/dist/provider-acp-capabilities.d.ts +52 -0
- package/dist/provider-acp-capabilities.js +101 -0
- package/dist/provider-admin-tools.d.ts +100 -0
- package/dist/provider-admin-tools.js +572 -0
- package/dist/provider-capability-cache.d.ts +46 -0
- package/dist/provider-capability-cache.js +248 -0
- package/dist/provider-capability-discovery.d.ts +85 -0
- package/dist/provider-capability-discovery.js +461 -0
- package/dist/provider-capability-resolver.d.ts +29 -0
- package/dist/provider-capability-resolver.js +92 -0
- package/dist/provider-definition-assertions.d.ts +9 -0
- package/dist/provider-definition-assertions.js +147 -0
- package/dist/provider-definitions.d.ts +127 -0
- package/dist/provider-definitions.js +758 -0
- package/dist/provider-help-parser.d.ts +34 -0
- package/dist/provider-help-parser.js +203 -0
- package/dist/provider-model-discovery.d.ts +30 -0
- package/dist/provider-model-discovery.js +229 -0
- package/dist/provider-output-metadata.d.ts +7 -0
- package/dist/provider-output-metadata.js +68 -0
- package/dist/provider-schema-builder.d.ts +22 -0
- package/dist/provider-schema-builder.js +55 -0
- package/dist/provider-surface-generator.d.ts +98 -0
- package/dist/provider-surface-generator.js +140 -0
- package/dist/provider-tool-capabilities.d.ts +3 -2
- package/dist/provider-tool-capabilities.js +77 -62
- package/dist/remote-url.d.ts +28 -0
- package/dist/remote-url.js +61 -0
- package/dist/request-helpers.d.ts +37 -4
- package/dist/request-helpers.js +134 -0
- package/dist/resources.d.ts +6 -1
- package/dist/resources.js +67 -193
- package/dist/session-manager.d.ts +2 -0
- package/dist/session-manager.js +34 -8
- package/dist/stream-json-parser.d.ts +1 -0
- package/dist/stream-json-parser.js +3 -0
- package/dist/upstream-contracts.d.ts +17 -1
- package/dist/upstream-contracts.js +209 -36
- package/dist/workspace-registry.d.ts +9 -0
- package/dist/workspace-registry.js +24 -4
- package/npm-shrinkwrap.json +2 -2
- package/package.json +8 -3
- package/setup/status.schema.json +75 -0
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { gatherRemoteHttpOAuthReadiness } from "./doctor.js";
|
|
2
|
+
import { diagnoseRemoteOAuthConfig } from "./config.js";
|
|
3
|
+
import { joinBaseAndPath, resolveConfiguredRemoteOrigin } from "./remote-url.js";
|
|
4
|
+
export const CONNECTOR_SETUP_SECRET_WARNING = "Never paste gateway bearer tokens, OAuth client secrets, OAuth access tokens, consent/shared secrets, tunnel tokens, or provider credentials into a remote chat transcript. Only the fields in this packet are safe to paste into a connector UI.";
|
|
5
|
+
export function buildConnectorSetupPacket(input) {
|
|
6
|
+
const { readiness, oauth } = input;
|
|
7
|
+
const options = input.options ?? {};
|
|
8
|
+
const configuredClientId = oauth.clients[0]?.clientId ?? null;
|
|
9
|
+
const clientId = options.clientId ?? configuredClientId;
|
|
10
|
+
const clientSecretRequired = oauth.enabled && !oauth.allowPublicClients;
|
|
11
|
+
const clientSecretSource = clientSecretRequired
|
|
12
|
+
? "Run `llm-cli-gateway oauth client add <client-id> --redirect-uri <connector-callback> --print-once` (or `oauth client rotate <client-id> --print-once`) and paste the printed secret once into the connector UI."
|
|
13
|
+
: null;
|
|
14
|
+
const packet = {
|
|
15
|
+
ok: true,
|
|
16
|
+
schema: "remote-connector-setup.v1",
|
|
17
|
+
ready: readiness.ready,
|
|
18
|
+
stage: readiness.stage,
|
|
19
|
+
auth_mode: readiness.auth_mode,
|
|
20
|
+
connector: {
|
|
21
|
+
mcp_url: readiness.mcp_url,
|
|
22
|
+
authorization_url: readiness.oauth.authorization_url,
|
|
23
|
+
token_url: readiness.oauth.token_url,
|
|
24
|
+
client_id: clientId,
|
|
25
|
+
client_secret_required: clientSecretRequired,
|
|
26
|
+
client_secret_source: clientSecretSource,
|
|
27
|
+
},
|
|
28
|
+
workspace: readiness.workspace,
|
|
29
|
+
next_actions: readiness.next_actions,
|
|
30
|
+
warnings: [CONNECTOR_SETUP_SECRET_WARNING],
|
|
31
|
+
};
|
|
32
|
+
if (options.includeLegacyNoAuth) {
|
|
33
|
+
packet.legacy_no_auth = {
|
|
34
|
+
deprecated: true,
|
|
35
|
+
connector_url: input.legacyNoAuthUrl ?? null,
|
|
36
|
+
note: "Deprecated no-auth connector URL. It bypasses authentication; prefer OAuth. Do not share this URL.",
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return packet;
|
|
40
|
+
}
|
|
41
|
+
export function legacyNoAuthConnectorUrl(env = process.env) {
|
|
42
|
+
const noAuthPath = (env.LLM_GATEWAY_NO_AUTH_PATHS || "")
|
|
43
|
+
.split(/[,;\s]+/)
|
|
44
|
+
.map(value => value.trim())
|
|
45
|
+
.find(value => value.startsWith("/") && !value.includes("?") && !value.includes("#"));
|
|
46
|
+
if (!noAuthPath)
|
|
47
|
+
return null;
|
|
48
|
+
const baseOrigin = resolveConfiguredRemoteOrigin({
|
|
49
|
+
publicUrl: env.LLM_GATEWAY_PUBLIC_URL ?? null,
|
|
50
|
+
});
|
|
51
|
+
if (!baseOrigin)
|
|
52
|
+
return null;
|
|
53
|
+
return joinBaseAndPath(baseOrigin, noAuthPath);
|
|
54
|
+
}
|
|
55
|
+
export function gatherConnectorSetupPacket(options = {}, env = process.env) {
|
|
56
|
+
const readiness = gatherRemoteHttpOAuthReadiness(env);
|
|
57
|
+
const oauth = diagnoseRemoteOAuthConfig(undefined, env).config;
|
|
58
|
+
const legacyNoAuthUrl = options.includeLegacyNoAuth ? legacyNoAuthConnectorUrl(env) : null;
|
|
59
|
+
return buildConnectorSetupPacket({ readiness, oauth, options, legacyNoAuthUrl });
|
|
60
|
+
}
|
|
61
|
+
export function renderConnectorSetupSummary(packet) {
|
|
62
|
+
const lines = [];
|
|
63
|
+
lines.push(`Remote connector readiness: ${packet.stage}${packet.ready ? " (ready)" : ""}`);
|
|
64
|
+
lines.push(`Authentication mode: ${packet.auth_mode}`);
|
|
65
|
+
if (packet.connector.mcp_url)
|
|
66
|
+
lines.push(`MCP URL: ${packet.connector.mcp_url}`);
|
|
67
|
+
if (packet.connector.authorization_url)
|
|
68
|
+
lines.push(`Authorization URL: ${packet.connector.authorization_url}`);
|
|
69
|
+
if (packet.connector.token_url)
|
|
70
|
+
lines.push(`Token URL: ${packet.connector.token_url}`);
|
|
71
|
+
if (packet.connector.client_id)
|
|
72
|
+
lines.push(`Client ID: ${packet.connector.client_id}`);
|
|
73
|
+
if (packet.connector.client_secret_required && packet.connector.client_secret_source) {
|
|
74
|
+
lines.push(`Client secret: ${packet.connector.client_secret_source}`);
|
|
75
|
+
}
|
|
76
|
+
lines.push(`Workspace: ${packet.workspace.ready
|
|
77
|
+
? `ready (default: ${packet.workspace.default ?? "none, pass a registered alias"})`
|
|
78
|
+
: "not ready"}`);
|
|
79
|
+
if (packet.next_actions.length > 0) {
|
|
80
|
+
lines.push("Next actions:");
|
|
81
|
+
for (const action of packet.next_actions)
|
|
82
|
+
lines.push(` - ${action}`);
|
|
83
|
+
}
|
|
84
|
+
lines.push(packet.warnings[0] ?? CONNECTOR_SETUP_SECRET_WARNING);
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|
package/dist/doctor.d.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { type EndpointExposureReport } from "./endpoint-exposure.js";
|
|
|
2
2
|
import { type ProviderLoginStatus } from "./provider-status.js";
|
|
3
3
|
import { type ApiProviderLoginGuidance } from "./provider-login-guidance.js";
|
|
4
4
|
import type { FlightRecorderQuery } from "./flight-recorder.js";
|
|
5
|
-
import { type CacheAwarenessConfig, type ProvidersConfig } from "./config.js";
|
|
5
|
+
import { type CacheAwarenessConfig, type ProvidersConfig, type RemoteOAuthConfigDiagnostics } from "./config.js";
|
|
6
|
+
import type { AuthConfig } from "./auth.js";
|
|
7
|
+
import { type RemoteSafeWorkspaceSummary } from "./workspace-registry.js";
|
|
6
8
|
import { type ProviderCapabilityId, type ProviderKind } from "./provider-tool-capabilities.js";
|
|
7
9
|
import { type CliType } from "./session-manager.js";
|
|
8
10
|
export interface CacheAwarenessReport {
|
|
@@ -146,6 +148,7 @@ export interface DoctorReport {
|
|
|
146
148
|
};
|
|
147
149
|
}>;
|
|
148
150
|
endpoint_exposure: EndpointExposureReport;
|
|
151
|
+
remote_http_oauth: RemoteHttpOAuthReadiness;
|
|
149
152
|
client_config: {
|
|
150
153
|
claude_desktop_config_present: boolean;
|
|
151
154
|
codex_config_present: boolean;
|
|
@@ -167,6 +170,41 @@ export interface DoctorReport {
|
|
|
167
170
|
};
|
|
168
171
|
next_actions: string[];
|
|
169
172
|
}
|
|
173
|
+
export type RemoteHttpOAuthStage = "not_started" | "missing_public_url" | "endpoint_unreachable" | "oauth_disabled" | "unsafe_oauth_config" | "missing_oauth_client" | "missing_workspace" | "ready";
|
|
174
|
+
export declare const REMOTE_HTTP_OAUTH_STAGES: readonly RemoteHttpOAuthStage[];
|
|
175
|
+
export interface RemoteHttpOAuthReadiness {
|
|
176
|
+
ready: boolean;
|
|
177
|
+
stage: RemoteHttpOAuthStage;
|
|
178
|
+
public_url: string | null;
|
|
179
|
+
mcp_url: string | null;
|
|
180
|
+
auth_mode: "oauth" | "bearer_token" | "none";
|
|
181
|
+
oauth: {
|
|
182
|
+
enabled: boolean;
|
|
183
|
+
issuer: string | null;
|
|
184
|
+
authorization_url: string | null;
|
|
185
|
+
token_url: string | null;
|
|
186
|
+
registration_policy: string;
|
|
187
|
+
clients_configured: number;
|
|
188
|
+
consent_required: boolean;
|
|
189
|
+
};
|
|
190
|
+
workspace: {
|
|
191
|
+
ready: boolean;
|
|
192
|
+
default: string | null;
|
|
193
|
+
aliases: string[];
|
|
194
|
+
};
|
|
195
|
+
next_actions: string[];
|
|
196
|
+
}
|
|
197
|
+
export interface RemoteHttpOAuthReadinessInput {
|
|
198
|
+
oauthDiag: Pick<RemoteOAuthConfigDiagnostics, "status" | "config" | "issues">;
|
|
199
|
+
workspace: RemoteSafeWorkspaceSummary;
|
|
200
|
+
auth: AuthConfig;
|
|
201
|
+
transport: "stdio" | "http";
|
|
202
|
+
publicUrl: string | null;
|
|
203
|
+
endpoint: EndpointExposureReport;
|
|
204
|
+
mcpPath: string;
|
|
205
|
+
}
|
|
206
|
+
export declare function buildRemoteHttpOAuthReadiness(input: RemoteHttpOAuthReadinessInput): RemoteHttpOAuthReadiness;
|
|
207
|
+
export declare function gatherRemoteHttpOAuthReadiness(env?: NodeJS.ProcessEnv): RemoteHttpOAuthReadiness;
|
|
170
208
|
export interface CreateDoctorReportOptions {
|
|
171
209
|
env?: NodeJS.ProcessEnv;
|
|
172
210
|
flightRecorder?: FlightRecorderQuery;
|
package/dist/doctor.js
CHANGED
|
@@ -7,8 +7,9 @@ import { createEndpointExposureReport, redactDiagnosticUrl, } from "./endpoint-e
|
|
|
7
7
|
import { getApiProviderStatus, listProviderRuntimeStatuses, } from "./provider-status.js";
|
|
8
8
|
import { getApiProviderLoginGuidance, } from "./provider-login-guidance.js";
|
|
9
9
|
import { CLAUDE_MCP_SERVER_NAMES } from "./claude-mcp-config.js";
|
|
10
|
-
import { enabledApiProviders, loadCacheAwarenessConfig, loadProvidersConfig,
|
|
11
|
-
import { loadWorkspaceRegistry } from "./workspace-registry.js";
|
|
10
|
+
import { diagnoseRemoteOAuthConfig, enabledApiProviders, loadCacheAwarenessConfig, loadProvidersConfig, } from "./config.js";
|
|
11
|
+
import { loadWorkspaceRegistry, remoteSafeWorkspaceSummary, } from "./workspace-registry.js";
|
|
12
|
+
import { buildRemoteConnectorUrls, resolveConfiguredRemoteOrigin } from "./remote-url.js";
|
|
12
13
|
import { computeGlobalCacheStats } from "./cache-stats.js";
|
|
13
14
|
import { FlightRecorder, resolveFlightRecorderDbPath } from "./flight-recorder.js";
|
|
14
15
|
import { buildUpstreamContractReport } from "./upstream-contracts.js";
|
|
@@ -136,6 +137,174 @@ export function checkGeminiConfig(cwd = process.cwd(), home = homedir(), whiteli
|
|
|
136
137
|
next_actions: nextActions,
|
|
137
138
|
};
|
|
138
139
|
}
|
|
140
|
+
export const REMOTE_HTTP_OAUTH_STAGES = [
|
|
141
|
+
"not_started",
|
|
142
|
+
"missing_public_url",
|
|
143
|
+
"endpoint_unreachable",
|
|
144
|
+
"oauth_disabled",
|
|
145
|
+
"unsafe_oauth_config",
|
|
146
|
+
"missing_oauth_client",
|
|
147
|
+
"missing_workspace",
|
|
148
|
+
"ready",
|
|
149
|
+
];
|
|
150
|
+
export function buildRemoteHttpOAuthReadiness(input) {
|
|
151
|
+
const { config: oauth, status, issues } = input.oauthDiag;
|
|
152
|
+
const e = input.endpoint;
|
|
153
|
+
const baseOrigin = resolveConfiguredRemoteOrigin({
|
|
154
|
+
issuer: oauth.issuer === "auto" ? null : redactDiagnosticUrl(oauth.issuer),
|
|
155
|
+
publicUrl: input.publicUrl,
|
|
156
|
+
});
|
|
157
|
+
const urls = buildRemoteConnectorUrls({
|
|
158
|
+
baseOrigin,
|
|
159
|
+
mcpPath: input.mcpPath,
|
|
160
|
+
oauthEnabled: oauth.enabled,
|
|
161
|
+
});
|
|
162
|
+
const hasValidPublicHttpsUrl = e.public_url_configured &&
|
|
163
|
+
e.https_configured &&
|
|
164
|
+
(e.mode === "tunnel" || e.mode === "byo_reverse_proxy");
|
|
165
|
+
const oauthNotEnabled = status === "absent" || status === "disabled";
|
|
166
|
+
const oauthUnsafeForRemote = status === "malformed" ||
|
|
167
|
+
(oauth.enabled && (oauth.allowPublicClients || oauth.registrationPolicy === "open_dev"));
|
|
168
|
+
let stage;
|
|
169
|
+
if (!e.public_url_configured && input.transport !== "http" && status === "absent") {
|
|
170
|
+
stage = "not_started";
|
|
171
|
+
}
|
|
172
|
+
else if (!hasValidPublicHttpsUrl) {
|
|
173
|
+
stage = "missing_public_url";
|
|
174
|
+
}
|
|
175
|
+
else if (e.reachable_from_web === "unreachable") {
|
|
176
|
+
stage = "endpoint_unreachable";
|
|
177
|
+
}
|
|
178
|
+
else if (oauthNotEnabled) {
|
|
179
|
+
stage = "oauth_disabled";
|
|
180
|
+
}
|
|
181
|
+
else if (oauthUnsafeForRemote) {
|
|
182
|
+
stage = "unsafe_oauth_config";
|
|
183
|
+
}
|
|
184
|
+
else if (oauth.registrationPolicy === "static_clients" && oauth.clients.length === 0) {
|
|
185
|
+
stage = "missing_oauth_client";
|
|
186
|
+
}
|
|
187
|
+
else if (!input.workspace.ready) {
|
|
188
|
+
stage = "missing_workspace";
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
stage = "ready";
|
|
192
|
+
}
|
|
193
|
+
const authMode = oauth.enabled
|
|
194
|
+
? "oauth"
|
|
195
|
+
: input.auth.required && input.auth.tokenConfigured
|
|
196
|
+
? "bearer_token"
|
|
197
|
+
: "none";
|
|
198
|
+
return {
|
|
199
|
+
ready: stage === "ready",
|
|
200
|
+
stage,
|
|
201
|
+
public_url: input.publicUrl,
|
|
202
|
+
mcp_url: urls.mcpUrl,
|
|
203
|
+
auth_mode: authMode,
|
|
204
|
+
oauth: {
|
|
205
|
+
enabled: oauth.enabled,
|
|
206
|
+
issuer: urls.issuer,
|
|
207
|
+
authorization_url: urls.authorizationUrl,
|
|
208
|
+
token_url: urls.tokenUrl,
|
|
209
|
+
registration_policy: oauth.registrationPolicy,
|
|
210
|
+
clients_configured: oauth.clients.length,
|
|
211
|
+
consent_required: oauth.requireConsent,
|
|
212
|
+
},
|
|
213
|
+
workspace: {
|
|
214
|
+
ready: input.workspace.ready,
|
|
215
|
+
default: input.workspace.default,
|
|
216
|
+
aliases: input.workspace.aliases,
|
|
217
|
+
},
|
|
218
|
+
next_actions: remoteReadinessNextActions(stage, {
|
|
219
|
+
oauth,
|
|
220
|
+
status,
|
|
221
|
+
issues,
|
|
222
|
+
workspace: input.workspace,
|
|
223
|
+
endpointReachability: e.reachable_from_web,
|
|
224
|
+
}),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function remoteReadinessNextActions(stage, ctx) {
|
|
228
|
+
switch (stage) {
|
|
229
|
+
case "not_started":
|
|
230
|
+
return [
|
|
231
|
+
"Remote HTTP + OAuth setup has not started. Start an HTTPS tunnel or reverse proxy and set LLM_GATEWAY_PUBLIC_URL to the public https URL.",
|
|
232
|
+
"Then register an OAuth client: llm-cli-gateway oauth client add <client-id> --redirect-uri <connector-callback> --print-once",
|
|
233
|
+
];
|
|
234
|
+
case "missing_public_url":
|
|
235
|
+
return [
|
|
236
|
+
"Set LLM_GATEWAY_PUBLIC_URL to a public https URL (tunnel or reverse proxy), not localhost or a LAN address.",
|
|
237
|
+
"Re-run: llm-cli-gateway doctor --json",
|
|
238
|
+
];
|
|
239
|
+
case "endpoint_unreachable":
|
|
240
|
+
return [
|
|
241
|
+
"The public MCP URL is configured but not reachable from the web. Fix tunnel/reverse-proxy routing, then re-run: llm-cli-gateway doctor --json",
|
|
242
|
+
];
|
|
243
|
+
case "oauth_disabled":
|
|
244
|
+
return [
|
|
245
|
+
"OAuth is the recommended remote connector authentication mode. Enable it by registering a client: llm-cli-gateway oauth client add <client-id> --redirect-uri <connector-callback> --print-once",
|
|
246
|
+
];
|
|
247
|
+
case "unsafe_oauth_config": {
|
|
248
|
+
const reasons = [];
|
|
249
|
+
if (ctx.status === "malformed") {
|
|
250
|
+
reasons.push(...(ctx.issues.length
|
|
251
|
+
? ctx.issues
|
|
252
|
+
: ["The [http.oauth] config is invalid; fix it and re-run doctor --json."]));
|
|
253
|
+
}
|
|
254
|
+
else {
|
|
255
|
+
if (ctx.oauth.allowPublicClients) {
|
|
256
|
+
reasons.push("OAuth allow_public_clients is enabled on a public endpoint. Use confidential clients: registration_policy=static_clients with a client secret.");
|
|
257
|
+
}
|
|
258
|
+
if (ctx.oauth.registrationPolicy === "open_dev") {
|
|
259
|
+
reasons.push("OAuth registration_policy=open_dev is unsafe on a public endpoint. Switch to static_clients with confidential client secrets.");
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
reasons.push("Re-run: llm-cli-gateway doctor --json");
|
|
263
|
+
return reasons;
|
|
264
|
+
}
|
|
265
|
+
case "missing_oauth_client":
|
|
266
|
+
return [
|
|
267
|
+
"OAuth is enabled but no client is registered. Add one: llm-cli-gateway oauth client add <client-id> --redirect-uri <connector-callback> --print-once",
|
|
268
|
+
];
|
|
269
|
+
case "missing_workspace":
|
|
270
|
+
return [
|
|
271
|
+
"No workspace is available for remote provider execution. Register a repo and set it as the default: add a [[workspaces.repos]] entry with [workspaces].default in ~/.llm-cli-gateway/config.toml, or run `llm-cli-gateway workspace add <alias> <absolute-repo-path> --default` when an allowed root is configured.",
|
|
272
|
+
"Remote clients then select the workspace by alias; local absolute paths are never accepted from remote clients.",
|
|
273
|
+
];
|
|
274
|
+
case "ready": {
|
|
275
|
+
const readyActions = [
|
|
276
|
+
ctx.workspace.default
|
|
277
|
+
? `Remote connector is ready. Remote provider calls use the default workspace "${ctx.workspace.default}" unless a registered alias is supplied.`
|
|
278
|
+
: "Remote connector is ready. Remote provider calls must supply a registered workspace alias; set a default with `llm-cli-gateway workspace add <alias> <path> --default` to make one implicit.",
|
|
279
|
+
"Paste only copy-safe connector fields (MCP URL, authorization URL, token URL, client id) into the remote connector UI.",
|
|
280
|
+
];
|
|
281
|
+
if (ctx.endpointReachability === "not_checked") {
|
|
282
|
+
readyActions.push("Config is complete but public reachability was not verified. Set LLM_GATEWAY_VERIFY_PUBLIC_URL=1 and re-run doctor --json to confirm the public URL is reachable from the web.");
|
|
283
|
+
}
|
|
284
|
+
return readyActions;
|
|
285
|
+
}
|
|
286
|
+
default:
|
|
287
|
+
return [];
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
export function gatherRemoteHttpOAuthReadiness(env = process.env) {
|
|
291
|
+
const oauthDiag = diagnoseRemoteOAuthConfig(undefined, env);
|
|
292
|
+
const workspace = remoteSafeWorkspaceSummary(loadWorkspaceRegistry());
|
|
293
|
+
const auth = loadAuthConfig(env);
|
|
294
|
+
const transport = defaultTransport(env);
|
|
295
|
+
const publicUrl = redactDiagnosticUrl(env.LLM_GATEWAY_PUBLIC_URL || null);
|
|
296
|
+
const endpoint = createEndpointExposureReport(env, publicUrl);
|
|
297
|
+
const mcpPath = env.LLM_GATEWAY_HTTP_PATH || "/mcp";
|
|
298
|
+
return buildRemoteHttpOAuthReadiness({
|
|
299
|
+
oauthDiag,
|
|
300
|
+
workspace,
|
|
301
|
+
auth,
|
|
302
|
+
transport,
|
|
303
|
+
publicUrl,
|
|
304
|
+
endpoint,
|
|
305
|
+
mcpPath,
|
|
306
|
+
});
|
|
307
|
+
}
|
|
139
308
|
function packageVersion() {
|
|
140
309
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
141
310
|
const candidates = [join(here, "..", "package.json"), join(here, "..", "..", "package.json")];
|
|
@@ -306,7 +475,8 @@ export function createDoctorReport(envOrOptions = process.env) {
|
|
|
306
475
|
: { env: envOrOptions };
|
|
307
476
|
const env = opts.env ?? process.env;
|
|
308
477
|
const auth = loadAuthConfig(env);
|
|
309
|
-
const
|
|
478
|
+
const oauthDiag = diagnoseRemoteOAuthConfig(undefined, env);
|
|
479
|
+
const oauth = oauthDiag.config;
|
|
310
480
|
const workspaceRegistry = loadWorkspaceRegistry();
|
|
311
481
|
const transport = defaultTransport(env);
|
|
312
482
|
const rawPublicUrl = env.LLM_GATEWAY_PUBLIC_URL || null;
|
|
@@ -382,6 +552,15 @@ export function createDoctorReport(envOrOptions = process.env) {
|
|
|
382
552
|
},
|
|
383
553
|
providers: Object.fromEntries(CLI_TYPES.map(provider => [provider, doctorProviderStatus(providerStatuses[provider])])),
|
|
384
554
|
endpoint_exposure: endpointExposure,
|
|
555
|
+
remote_http_oauth: buildRemoteHttpOAuthReadiness({
|
|
556
|
+
oauthDiag,
|
|
557
|
+
workspace: remoteSafeWorkspaceSummary(workspaceRegistry),
|
|
558
|
+
auth,
|
|
559
|
+
transport,
|
|
560
|
+
publicUrl,
|
|
561
|
+
endpoint: endpointExposure,
|
|
562
|
+
mcpPath: env.LLM_GATEWAY_HTTP_PATH || "/mcp",
|
|
563
|
+
}),
|
|
385
564
|
client_config: clientConfigStatus(),
|
|
386
565
|
cache_awareness: buildCacheAwarenessReport(opts),
|
|
387
566
|
provider_capabilities: buildProviderCapabilitySummary(providerStatuses),
|
package/dist/flight-recorder.js
CHANGED
|
@@ -54,6 +54,16 @@ function ensureCacheControlTtlSecondsColumn(db) {
|
|
|
54
54
|
db.exec("ALTER TABLE requests ADD COLUMN cache_control_ttl_seconds INTEGER");
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
|
+
function ensureMetadataProviderSessionColumns(db) {
|
|
58
|
+
const rows = db.prepare("PRAGMA table_info(gateway_metadata)").all();
|
|
59
|
+
const names = new Set(rows.map((row) => (row && typeof row.name === "string" ? row.name : "")));
|
|
60
|
+
if (!names.has("provider_session_id")) {
|
|
61
|
+
db.exec("ALTER TABLE gateway_metadata ADD COLUMN provider_session_id TEXT");
|
|
62
|
+
}
|
|
63
|
+
if (!names.has("stop_reason")) {
|
|
64
|
+
db.exec("ALTER TABLE gateway_metadata ADD COLUMN stop_reason TEXT");
|
|
65
|
+
}
|
|
66
|
+
}
|
|
57
67
|
export function resolveFlightRecorderDbPath() {
|
|
58
68
|
const configured = process.env.LLM_GATEWAY_LOGS_DB;
|
|
59
69
|
if (configured !== undefined) {
|
|
@@ -147,6 +157,8 @@ export class FlightRecorder {
|
|
|
147
157
|
http_status INTEGER,
|
|
148
158
|
error_message TEXT,
|
|
149
159
|
async_job_id TEXT,
|
|
160
|
+
provider_session_id TEXT,
|
|
161
|
+
stop_reason TEXT,
|
|
150
162
|
status TEXT NOT NULL DEFAULT 'started'
|
|
151
163
|
);
|
|
152
164
|
|
|
@@ -180,6 +192,10 @@ export class FlightRecorder {
|
|
|
180
192
|
this.db
|
|
181
193
|
.prepare("INSERT OR IGNORE INTO _migrations(version, applied_at) VALUES(6, ?)")
|
|
182
194
|
.run(new Date().toISOString());
|
|
195
|
+
ensureMetadataProviderSessionColumns(this.db);
|
|
196
|
+
this.db
|
|
197
|
+
.prepare("INSERT OR IGNORE INTO _migrations(version, applied_at) VALUES(7, ?)")
|
|
198
|
+
.run(new Date().toISOString());
|
|
183
199
|
if (process.platform !== "win32") {
|
|
184
200
|
try {
|
|
185
201
|
chmodSync(dbPath, 0o600);
|
|
@@ -240,6 +256,8 @@ export class FlightRecorder {
|
|
|
240
256
|
exit_code = @exit_code,
|
|
241
257
|
http_status = @http_status,
|
|
242
258
|
error_message = @error_message,
|
|
259
|
+
provider_session_id = @provider_session_id,
|
|
260
|
+
stop_reason = @stop_reason,
|
|
243
261
|
status = @status
|
|
244
262
|
WHERE request_id = @id AND status = 'started'
|
|
245
263
|
`);
|
|
@@ -267,6 +285,8 @@ export class FlightRecorder {
|
|
|
267
285
|
exit_code: result.exitCode,
|
|
268
286
|
http_status: result.httpStatus ?? null,
|
|
269
287
|
error_message: result.errorMessage ?? null,
|
|
288
|
+
provider_session_id: result.providerSessionId ?? null,
|
|
289
|
+
stop_reason: result.stopReason ?? null,
|
|
270
290
|
status: result.status,
|
|
271
291
|
});
|
|
272
292
|
});
|
|
@@ -6,6 +6,8 @@ export interface GeminiUsage {
|
|
|
6
6
|
export interface GeminiJsonParseResult {
|
|
7
7
|
usage?: GeminiUsage;
|
|
8
8
|
response?: string;
|
|
9
|
+
sessionId?: string;
|
|
10
|
+
stopReason?: string;
|
|
9
11
|
}
|
|
10
12
|
export declare function parseGeminiJson(stdout: string): GeminiJsonParseResult | null;
|
|
11
13
|
export declare function parseGeminiStreamJson(stdout: string): GeminiJsonParseResult | null;
|
|
@@ -1,22 +1,47 @@
|
|
|
1
|
-
|
|
2
|
-
const trimmed = stdout.trim();
|
|
3
|
-
if (!trimmed) {
|
|
4
|
-
return null;
|
|
5
|
-
}
|
|
6
|
-
let parsed;
|
|
1
|
+
function parseTolerantJsonObject(text) {
|
|
7
2
|
try {
|
|
8
|
-
|
|
3
|
+
const v = JSON.parse(text);
|
|
4
|
+
if (v && typeof v === "object")
|
|
5
|
+
return v;
|
|
9
6
|
}
|
|
10
7
|
catch {
|
|
8
|
+
}
|
|
9
|
+
const start = text.indexOf("{");
|
|
10
|
+
const end = text.lastIndexOf("}");
|
|
11
|
+
if (start >= 0 && end > start) {
|
|
12
|
+
try {
|
|
13
|
+
const v = JSON.parse(text.slice(start, end + 1));
|
|
14
|
+
if (v && typeof v === "object")
|
|
15
|
+
return v;
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
export function parseGeminiJson(stdout) {
|
|
24
|
+
const trimmed = stdout.trim();
|
|
25
|
+
if (!trimmed) {
|
|
11
26
|
return null;
|
|
12
27
|
}
|
|
13
|
-
|
|
28
|
+
const parsed = parseTolerantJsonObject(trimmed);
|
|
29
|
+
if (!parsed) {
|
|
14
30
|
return null;
|
|
15
31
|
}
|
|
16
32
|
const result = {};
|
|
17
33
|
if (typeof parsed.response === "string") {
|
|
18
34
|
result.response = parsed.response;
|
|
19
35
|
}
|
|
36
|
+
if (typeof parsed.session_id === "string") {
|
|
37
|
+
result.sessionId = parsed.session_id;
|
|
38
|
+
}
|
|
39
|
+
else if (typeof parsed.sessionId === "string") {
|
|
40
|
+
result.sessionId = parsed.sessionId;
|
|
41
|
+
}
|
|
42
|
+
if (typeof parsed.status === "string") {
|
|
43
|
+
result.stopReason = parsed.status;
|
|
44
|
+
}
|
|
20
45
|
const meta = parsed.usageMetadata;
|
|
21
46
|
if (meta && typeof meta === "object") {
|
|
22
47
|
const input = typeof meta.promptTokenCount === "number" ? meta.promptTokenCount : undefined;
|
|
@@ -56,12 +81,24 @@ export function parseGeminiStreamJson(stdout) {
|
|
|
56
81
|
if (!event || typeof event !== "object")
|
|
57
82
|
continue;
|
|
58
83
|
sawAnyLine = true;
|
|
84
|
+
if (event.type === "init" && typeof event.session_id === "string") {
|
|
85
|
+
result.sessionId = event.session_id;
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
59
88
|
if (event.type === "message" &&
|
|
60
89
|
event.role === "assistant" &&
|
|
61
90
|
typeof event.content === "string") {
|
|
91
|
+
if (event.delta === false || event.is_delta === false) {
|
|
92
|
+
assistantChunks.length = 0;
|
|
93
|
+
}
|
|
62
94
|
assistantChunks.push(event.content);
|
|
63
95
|
continue;
|
|
64
96
|
}
|
|
97
|
+
if (event.type === "result") {
|
|
98
|
+
if (typeof event.status === "string") {
|
|
99
|
+
result.stopReason = event.status;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
65
102
|
if (event.type === "result" && event.stats && typeof event.stats === "object") {
|
|
66
103
|
const stats = event.stats;
|
|
67
104
|
const input = typeof stats.input_tokens === "number" ? stats.input_tokens : undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export interface GrokJsonParseResult {
|
|
2
|
+
text?: string;
|
|
3
|
+
stopReason?: string;
|
|
4
|
+
sessionId?: string;
|
|
5
|
+
requestId?: string;
|
|
6
|
+
thought?: string;
|
|
7
|
+
error?: string;
|
|
8
|
+
sawEvent?: boolean;
|
|
9
|
+
usageAbsent: true;
|
|
10
|
+
}
|
|
11
|
+
export declare function parseGrokJson(stdout: string): GrokJsonParseResult | null;
|
|
12
|
+
export declare function parseGrokStreamingJson(stdout: string): GrokJsonParseResult | null;
|
|
13
|
+
export declare function parseGrokOutput(outputFormat: string | undefined, stdout: string): GrokJsonParseResult | null;
|
|
14
|
+
export declare function grokDisplayText(outputFormat: string | undefined, stdout: string): string;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
function str(value) {
|
|
2
|
+
return typeof value === "string" ? value : undefined;
|
|
3
|
+
}
|
|
4
|
+
function deltaText(event) {
|
|
5
|
+
return str(event.data) ?? str(event.text) ?? str(event.content) ?? str(event.delta);
|
|
6
|
+
}
|
|
7
|
+
function deltaThought(event) {
|
|
8
|
+
return (str(event.data) ??
|
|
9
|
+
str(event.thought) ??
|
|
10
|
+
str(event.text) ??
|
|
11
|
+
str(event.content) ??
|
|
12
|
+
str(event.delta));
|
|
13
|
+
}
|
|
14
|
+
function parseTolerantGrokObject(text) {
|
|
15
|
+
try {
|
|
16
|
+
const v = JSON.parse(text);
|
|
17
|
+
if (v && typeof v === "object" && !Array.isArray(v))
|
|
18
|
+
return v;
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
}
|
|
22
|
+
const start = text.indexOf("{");
|
|
23
|
+
const end = text.lastIndexOf("}");
|
|
24
|
+
if (start >= 0 && end > start) {
|
|
25
|
+
try {
|
|
26
|
+
const v = JSON.parse(text.slice(start, end + 1));
|
|
27
|
+
if (v && typeof v === "object" && !Array.isArray(v))
|
|
28
|
+
return v;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
export function parseGrokJson(stdout) {
|
|
37
|
+
const trimmed = stdout.trim();
|
|
38
|
+
if (!trimmed) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
const parsed = parseTolerantGrokObject(trimmed);
|
|
42
|
+
if (!parsed) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
const result = { sawEvent: true, usageAbsent: true };
|
|
46
|
+
const text = str(parsed.text);
|
|
47
|
+
if (text !== undefined)
|
|
48
|
+
result.text = text;
|
|
49
|
+
const stopReason = str(parsed.stopReason);
|
|
50
|
+
if (stopReason !== undefined)
|
|
51
|
+
result.stopReason = stopReason;
|
|
52
|
+
const sessionId = str(parsed.sessionId);
|
|
53
|
+
if (sessionId !== undefined)
|
|
54
|
+
result.sessionId = sessionId;
|
|
55
|
+
const requestId = str(parsed.requestId);
|
|
56
|
+
if (requestId !== undefined)
|
|
57
|
+
result.requestId = requestId;
|
|
58
|
+
const thought = str(parsed.thought);
|
|
59
|
+
if (thought !== undefined)
|
|
60
|
+
result.thought = thought;
|
|
61
|
+
const error = str(parsed.error);
|
|
62
|
+
if (error !== undefined)
|
|
63
|
+
result.error = error;
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
export function parseGrokStreamingJson(stdout) {
|
|
67
|
+
if (!stdout) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const lines = stdout.split(/\r?\n/);
|
|
71
|
+
const result = { usageAbsent: true };
|
|
72
|
+
const textChunks = [];
|
|
73
|
+
const thoughtChunks = [];
|
|
74
|
+
let sawAnyLine = false;
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
const trimmed = line.trim();
|
|
77
|
+
if (!trimmed)
|
|
78
|
+
continue;
|
|
79
|
+
let event;
|
|
80
|
+
try {
|
|
81
|
+
event = JSON.parse(trimmed);
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
if (!event || typeof event !== "object" || Array.isArray(event))
|
|
87
|
+
continue;
|
|
88
|
+
sawAnyLine = true;
|
|
89
|
+
switch (str(event.type)) {
|
|
90
|
+
case "text": {
|
|
91
|
+
const t = deltaText(event);
|
|
92
|
+
if (t !== undefined)
|
|
93
|
+
textChunks.push(t);
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "thought": {
|
|
97
|
+
const t = deltaThought(event);
|
|
98
|
+
if (t !== undefined)
|
|
99
|
+
thoughtChunks.push(t);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "end": {
|
|
103
|
+
const stopReason = str(event.stopReason);
|
|
104
|
+
if (stopReason !== undefined)
|
|
105
|
+
result.stopReason = stopReason;
|
|
106
|
+
const sessionId = str(event.sessionId);
|
|
107
|
+
if (sessionId !== undefined)
|
|
108
|
+
result.sessionId = sessionId;
|
|
109
|
+
const requestId = str(event.requestId);
|
|
110
|
+
if (requestId !== undefined)
|
|
111
|
+
result.requestId = requestId;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case "error": {
|
|
115
|
+
const error = str(event.error) ?? str(event.message);
|
|
116
|
+
if (error !== undefined)
|
|
117
|
+
result.error = error;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
default:
|
|
121
|
+
break;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
if (!sawAnyLine) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
result.sawEvent = true;
|
|
128
|
+
if (textChunks.length > 0)
|
|
129
|
+
result.text = textChunks.join("");
|
|
130
|
+
if (thoughtChunks.length > 0)
|
|
131
|
+
result.thought = thoughtChunks.join("");
|
|
132
|
+
return result;
|
|
133
|
+
}
|
|
134
|
+
export function parseGrokOutput(outputFormat, stdout) {
|
|
135
|
+
if (outputFormat === "json")
|
|
136
|
+
return parseGrokJson(stdout);
|
|
137
|
+
if (outputFormat === "streaming-json")
|
|
138
|
+
return parseGrokStreamingJson(stdout);
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
export function grokDisplayText(outputFormat, stdout) {
|
|
142
|
+
if (outputFormat === "json") {
|
|
143
|
+
return stdout;
|
|
144
|
+
}
|
|
145
|
+
if (outputFormat === "streaming-json") {
|
|
146
|
+
const parsed = parseGrokStreamingJson(stdout);
|
|
147
|
+
if (!parsed)
|
|
148
|
+
return stdout;
|
|
149
|
+
if (parsed.text !== undefined)
|
|
150
|
+
return parsed.text;
|
|
151
|
+
if (parsed.error !== undefined)
|
|
152
|
+
return parsed.error;
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
return stdout;
|
|
156
|
+
}
|