llm-cli-gateway 2.13.2 → 2.14.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 +139 -0
- package/README.md +68 -29
- 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 +38 -1
- package/dist/async-job-manager.js +287 -20
- package/dist/codex-json-parser.d.ts +1 -0
- package/dist/codex-json-parser.js +6 -0
- package/dist/config.d.ts +19 -0
- package/dist/config.js +84 -1
- 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/index.d.ts +47 -2
- package/dist/index.js +504 -122
- package/dist/job-store.d.ts +119 -11
- package/dist/job-store.js +372 -42
- package/dist/model-registry.d.ts +1 -0
- package/dist/model-registry.js +46 -0
- package/dist/oauth.js +2 -2
- package/dist/postgres-job-store-worker.d.ts +1 -0
- package/dist/postgres-job-store-worker.js +444 -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/request-helpers.d.ts +37 -4
- package/dist/request-helpers.js +134 -0
- package/dist/resources.d.ts +6 -1
- package/dist/resources.js +64 -192
- package/dist/session-manager.js +18 -11
- package/dist/sqlite-driver.d.ts +1 -1
- package/dist/sqlite-driver.js +2 -1
- 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/npm-shrinkwrap.json +2 -2
- package/package.json +8 -3
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type ParsedFlagArity = "none" | "one" | "many";
|
|
2
|
+
export type ParsedFlagValueType = "boolean" | "string" | "enum" | "number" | "unknown";
|
|
3
|
+
export type ParsedFlagSafety = "safe" | "dangerous" | "unknown";
|
|
4
|
+
export type ParsedArgvPlacement = "flag" | "flag-then-value";
|
|
5
|
+
export interface ParsedFlag {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly aliases: readonly string[];
|
|
8
|
+
readonly arity: ParsedFlagArity;
|
|
9
|
+
readonly valueType: ParsedFlagValueType;
|
|
10
|
+
readonly enumValues: readonly string[];
|
|
11
|
+
readonly repeatable: boolean;
|
|
12
|
+
readonly safety: ParsedFlagSafety;
|
|
13
|
+
readonly argvPlacement: ParsedArgvPlacement;
|
|
14
|
+
readonly description: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ParsedSubcommand {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly description: string;
|
|
19
|
+
readonly helpChecksum: string;
|
|
20
|
+
}
|
|
21
|
+
export interface DiscoveredUnmapped {
|
|
22
|
+
readonly kind: "flag" | "subcommand" | "acp-method" | "model";
|
|
23
|
+
readonly raw: string;
|
|
24
|
+
readonly checksum: string;
|
|
25
|
+
readonly reason: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ParsedHelp {
|
|
28
|
+
readonly flags: readonly ParsedFlag[];
|
|
29
|
+
readonly subcommands: readonly ParsedSubcommand[];
|
|
30
|
+
readonly discoveredUnmapped: readonly DiscoveredUnmapped[];
|
|
31
|
+
readonly checksum: string;
|
|
32
|
+
}
|
|
33
|
+
export declare function checksumText(text: string): string;
|
|
34
|
+
export declare function parseHelpText(helpText: string): ParsedHelp;
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
export function checksumText(text) {
|
|
3
|
+
return createHash("sha256").update(text, "utf8").digest("hex");
|
|
4
|
+
}
|
|
5
|
+
const FLAG_TOKEN = /^-{1,2}[a-zA-Z][a-zA-Z0-9-]*$/;
|
|
6
|
+
const DANGEROUS_KEYWORDS = [
|
|
7
|
+
"dangerous",
|
|
8
|
+
"bypass",
|
|
9
|
+
"yolo",
|
|
10
|
+
"skip-permission",
|
|
11
|
+
"skip permissions",
|
|
12
|
+
"skip-git",
|
|
13
|
+
"no-sandbox",
|
|
14
|
+
"without sandbox",
|
|
15
|
+
"auto-approve",
|
|
16
|
+
"always-approve",
|
|
17
|
+
"disable-web-search",
|
|
18
|
+
"force",
|
|
19
|
+
];
|
|
20
|
+
const NUMBER_PLACEHOLDERS = /^(n|num|count|number|seconds|secs|ms|port|size|turns|budget|price|tokens|max[-_]?turns)$/i;
|
|
21
|
+
const PATH_PLACEHOLDERS = /^(file|files|dir|path|dir(ectory)?|glob)$/i;
|
|
22
|
+
const SUBCOMMAND_LINE = /^([a-zA-Z][a-zA-Z0-9_-]*)((?:\s+(?:<[^<>]*>|\[[^[\]]*\]|\.\.\.))*)(?:\s{2,}(\S.*)?)?$/;
|
|
23
|
+
function isDangerous(name, description) {
|
|
24
|
+
const haystack = `${name} ${description}`.toLowerCase();
|
|
25
|
+
return DANGEROUS_KEYWORDS.some(keyword => haystack.includes(keyword));
|
|
26
|
+
}
|
|
27
|
+
function extractEnumValues(fragment) {
|
|
28
|
+
const possible = /\bpossible values:\s*([^\]\n]+)/i.exec(fragment);
|
|
29
|
+
if (possible) {
|
|
30
|
+
return possible[1]
|
|
31
|
+
.split(",")
|
|
32
|
+
.map(value => value.trim())
|
|
33
|
+
.filter(value => value.length > 0);
|
|
34
|
+
}
|
|
35
|
+
const piped = /[<[]([a-zA-Z0-9._-]+(?:\s*\|\s*[a-zA-Z0-9._-]+)+)[\]>]/.exec(fragment);
|
|
36
|
+
if (piped) {
|
|
37
|
+
return piped[1]
|
|
38
|
+
.split("|")
|
|
39
|
+
.map(value => value.trim())
|
|
40
|
+
.filter(value => value.length > 0);
|
|
41
|
+
}
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
function hasValuePlaceholder(afterFlags) {
|
|
45
|
+
if (/[<[][^\]>]*[\]>]/.test(afterFlags))
|
|
46
|
+
return true;
|
|
47
|
+
if (/\b[A-Z][A-Z0-9_]{1,}\b/.test(afterFlags.split(/\s{2,}/, 1)[0] ?? ""))
|
|
48
|
+
return true;
|
|
49
|
+
if (/=/.test(afterFlags.split(/\s{2,}/, 1)[0] ?? ""))
|
|
50
|
+
return true;
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
function inferValueType(placeholder, enumValues) {
|
|
54
|
+
if (enumValues.length > 0)
|
|
55
|
+
return "enum";
|
|
56
|
+
const inner = /[<[]([^\]>]+)[\]>]/.exec(placeholder)?.[1]?.trim() ?? placeholder.trim();
|
|
57
|
+
const token = inner
|
|
58
|
+
.replace(/\.\.\.$/, "")
|
|
59
|
+
.replace(/[<>[\]]/g, "")
|
|
60
|
+
.trim();
|
|
61
|
+
if (token.length === 0)
|
|
62
|
+
return "string";
|
|
63
|
+
if (token.includes("="))
|
|
64
|
+
return "string";
|
|
65
|
+
if (NUMBER_PLACEHOLDERS.test(token))
|
|
66
|
+
return "number";
|
|
67
|
+
if (PATH_PLACEHOLDERS.test(token))
|
|
68
|
+
return "string";
|
|
69
|
+
if (/^[a-zA-Z0-9_./|-]+$/.test(token))
|
|
70
|
+
return "string";
|
|
71
|
+
return "unknown";
|
|
72
|
+
}
|
|
73
|
+
function splitFlagLine(line) {
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
if (!trimmed.startsWith("-"))
|
|
76
|
+
return null;
|
|
77
|
+
const gap = /\s{2,}/.exec(trimmed);
|
|
78
|
+
const declPart = gap ? trimmed.slice(0, gap.index) : trimmed;
|
|
79
|
+
const description = gap ? trimmed.slice(gap.index).trim() : "";
|
|
80
|
+
const pieces = declPart.split(/[,\s]+/).filter(Boolean);
|
|
81
|
+
const tokens = [];
|
|
82
|
+
let i = 0;
|
|
83
|
+
for (; i < pieces.length; i++) {
|
|
84
|
+
if (pieces[i].startsWith("-")) {
|
|
85
|
+
tokens.push(pieces[i]);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const afterFlags = pieces.slice(i).join(" ");
|
|
92
|
+
return { tokens, afterFlags, description };
|
|
93
|
+
}
|
|
94
|
+
export function parseHelpText(helpText) {
|
|
95
|
+
const normalized = helpText.replace(/\r\n/g, "\n");
|
|
96
|
+
const lines = normalized.split("\n");
|
|
97
|
+
const flags = [];
|
|
98
|
+
const subcommands = [];
|
|
99
|
+
const discoveredUnmapped = [];
|
|
100
|
+
const seenFlagNames = new Set();
|
|
101
|
+
const seenSubcommands = new Set();
|
|
102
|
+
let inSubcommandSection = false;
|
|
103
|
+
for (let index = 0; index < lines.length; index++) {
|
|
104
|
+
const line = lines[index];
|
|
105
|
+
const trimmed = line.trim();
|
|
106
|
+
if (trimmed.length === 0)
|
|
107
|
+
continue;
|
|
108
|
+
const lower = trimmed.toLowerCase();
|
|
109
|
+
if (/^(commands|available subcommands|subcommands):$/.test(lower)) {
|
|
110
|
+
inSubcommandSection = true;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (/^(options|arguments|flags):$/.test(lower) || lower.startsWith("usage")) {
|
|
114
|
+
inSubcommandSection = false;
|
|
115
|
+
if (lower.startsWith("usage"))
|
|
116
|
+
continue;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const decl = splitFlagLine(line);
|
|
120
|
+
if (inSubcommandSection && !decl) {
|
|
121
|
+
const match = SUBCOMMAND_LINE.exec(trimmed);
|
|
122
|
+
if (match) {
|
|
123
|
+
const name = match[1];
|
|
124
|
+
if (name === "help")
|
|
125
|
+
continue;
|
|
126
|
+
if (!seenSubcommands.has(name)) {
|
|
127
|
+
seenSubcommands.add(name);
|
|
128
|
+
const description = (match[3] ?? "").trim();
|
|
129
|
+
subcommands.push({
|
|
130
|
+
name,
|
|
131
|
+
description,
|
|
132
|
+
helpChecksum: checksumText(`${name}\t${description}`),
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (!decl)
|
|
139
|
+
continue;
|
|
140
|
+
if (decl.tokens.length === 0 || !decl.tokens.every(token => FLAG_TOKEN.test(token))) {
|
|
141
|
+
discoveredUnmapped.push({
|
|
142
|
+
kind: "flag",
|
|
143
|
+
raw: trimmed,
|
|
144
|
+
checksum: checksumText(trimmed),
|
|
145
|
+
reason: "help line begins with '-' but its leading token is not a parseable flag (non-ASCII or malformed flag name)",
|
|
146
|
+
});
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const longFlag = decl.tokens.find(token => token.startsWith("--"));
|
|
150
|
+
const name = longFlag ?? decl.tokens[0];
|
|
151
|
+
const aliases = decl.tokens.filter(token => token !== name);
|
|
152
|
+
if (seenFlagNames.has(name))
|
|
153
|
+
continue;
|
|
154
|
+
seenFlagNames.add(name);
|
|
155
|
+
let enumSource = `${decl.afterFlags} ${decl.description}`;
|
|
156
|
+
for (let j = index + 1; j < lines.length; j++) {
|
|
157
|
+
const cont = lines[j];
|
|
158
|
+
if (cont.trim().length === 0)
|
|
159
|
+
break;
|
|
160
|
+
const contDecl = splitFlagLine(cont);
|
|
161
|
+
if (contDecl && contDecl.tokens.length > 0)
|
|
162
|
+
break;
|
|
163
|
+
enumSource += ` ${cont.trim()}`;
|
|
164
|
+
if (/possible values:/i.test(cont))
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
const enumValues = extractEnumValues(enumSource);
|
|
168
|
+
const takesValue = hasValuePlaceholder(decl.afterFlags) || enumValues.length > 0;
|
|
169
|
+
const repeatable = /\.\.\./.test(decl.afterFlags) ||
|
|
170
|
+
/repeat/i.test(decl.description) ||
|
|
171
|
+
/repeatable/i.test(enumSource);
|
|
172
|
+
let arity;
|
|
173
|
+
if (!takesValue) {
|
|
174
|
+
arity = "none";
|
|
175
|
+
}
|
|
176
|
+
else if (repeatable) {
|
|
177
|
+
arity = "many";
|
|
178
|
+
}
|
|
179
|
+
else {
|
|
180
|
+
arity = "one";
|
|
181
|
+
}
|
|
182
|
+
const valueType = arity === "none" ? "boolean" : inferValueType(decl.afterFlags, enumValues);
|
|
183
|
+
const description = decl.description;
|
|
184
|
+
const safety = isDangerous(name, description) ? "dangerous" : "safe";
|
|
185
|
+
flags.push({
|
|
186
|
+
name,
|
|
187
|
+
aliases,
|
|
188
|
+
arity,
|
|
189
|
+
valueType,
|
|
190
|
+
enumValues,
|
|
191
|
+
repeatable,
|
|
192
|
+
safety,
|
|
193
|
+
argvPlacement: arity === "none" ? "flag" : "flag-then-value",
|
|
194
|
+
description,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
flags,
|
|
199
|
+
subcommands,
|
|
200
|
+
discoveredUnmapped,
|
|
201
|
+
checksum: checksumText(normalized.trim()),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CliType, ModelCatalogParseFormat, ModelFallbackPolicy, ProviderDefinition, ProviderModelConfigSource, ProviderModelFacts } from "./provider-definitions.js";
|
|
2
|
+
import type { DiscoveredCapabilitySet } from "./provider-capability-discovery.js";
|
|
3
|
+
import { type DiscoveredUnmapped } from "./provider-help-parser.js";
|
|
4
|
+
import type { CliInfo } from "./model-registry.js";
|
|
5
|
+
export type ModelOrigin = "live-catalog" | "live-hidden" | "account-label" | "config" | "env" | "curated-fallback";
|
|
6
|
+
export interface DiscoveredModel {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly label?: string;
|
|
9
|
+
readonly description?: string;
|
|
10
|
+
readonly origin: ModelOrigin;
|
|
11
|
+
readonly isDefault: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface DiscoveredModelListing {
|
|
14
|
+
readonly providerId: CliType;
|
|
15
|
+
readonly strategy: string;
|
|
16
|
+
readonly parse: ModelCatalogParseFormat;
|
|
17
|
+
readonly fallbackPolicy: ModelFallbackPolicy;
|
|
18
|
+
readonly source: "live-command" | "config-or-env" | "curated-catalog";
|
|
19
|
+
readonly defaultModel: string | null;
|
|
20
|
+
readonly models: readonly DiscoveredModel[];
|
|
21
|
+
readonly facts: ProviderModelFacts;
|
|
22
|
+
readonly configSources: readonly ProviderModelConfigSource[];
|
|
23
|
+
readonly catalogChecksum: string;
|
|
24
|
+
readonly discoveredUnmapped: readonly DiscoveredUnmapped[];
|
|
25
|
+
readonly evidence: string;
|
|
26
|
+
}
|
|
27
|
+
export interface ModelDiscoveryOptions {
|
|
28
|
+
readonly registryInfo?: CliInfo;
|
|
29
|
+
}
|
|
30
|
+
export declare function discoverProviderModels(def: ProviderDefinition, set: DiscoveredCapabilitySet, options?: ModelDiscoveryOptions): DiscoveredModelListing;
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { assertNever } from "./provider-definition-assertions.js";
|
|
2
|
+
import { checksumText } from "./provider-help-parser.js";
|
|
3
|
+
import { scrubString } from "./provider-capability-cache.js";
|
|
4
|
+
function shortDescription(value) {
|
|
5
|
+
if (typeof value !== "string")
|
|
6
|
+
return undefined;
|
|
7
|
+
const trimmed = value.trim();
|
|
8
|
+
if (trimmed.length === 0)
|
|
9
|
+
return undefined;
|
|
10
|
+
return trimmed.length > 200 ? `${trimmed.slice(0, 197)}...` : trimmed;
|
|
11
|
+
}
|
|
12
|
+
function isPlausibleModelId(value) {
|
|
13
|
+
return /^[A-Za-z0-9][A-Za-z0-9._/-]{1,80}$/.test(value);
|
|
14
|
+
}
|
|
15
|
+
const EMPTY_NATIVE = { models: [], defaultModel: null, unmapped: [] };
|
|
16
|
+
function parseCodexDebugJson(raw) {
|
|
17
|
+
const models = [];
|
|
18
|
+
const unmapped = [];
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(raw);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
unmapped.push({
|
|
25
|
+
kind: "model",
|
|
26
|
+
raw: raw.slice(0, 120),
|
|
27
|
+
checksum: checksumText(raw),
|
|
28
|
+
reason: "codex debug models output is not valid JSON",
|
|
29
|
+
});
|
|
30
|
+
return { models, defaultModel: null, unmapped };
|
|
31
|
+
}
|
|
32
|
+
const list = parsed && typeof parsed === "object" && Array.isArray(parsed.models)
|
|
33
|
+
? parsed.models
|
|
34
|
+
: null;
|
|
35
|
+
if (!list) {
|
|
36
|
+
unmapped.push({
|
|
37
|
+
kind: "model",
|
|
38
|
+
raw: JSON.stringify(parsed).slice(0, 120),
|
|
39
|
+
checksum: checksumText(raw),
|
|
40
|
+
reason: "codex debug models JSON has no models[] array",
|
|
41
|
+
});
|
|
42
|
+
return { models, defaultModel: null, unmapped };
|
|
43
|
+
}
|
|
44
|
+
for (const entry of list) {
|
|
45
|
+
if (!entry || typeof entry !== "object")
|
|
46
|
+
continue;
|
|
47
|
+
const record = entry;
|
|
48
|
+
const slug = typeof record.slug === "string" ? record.slug.trim() : "";
|
|
49
|
+
if (!slug || !isPlausibleModelId(slug)) {
|
|
50
|
+
unmapped.push({
|
|
51
|
+
kind: "model",
|
|
52
|
+
raw: JSON.stringify(record).slice(0, 120),
|
|
53
|
+
checksum: checksumText(JSON.stringify(record)),
|
|
54
|
+
reason: "codex model entry has a missing or non-id slug",
|
|
55
|
+
});
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const origin = record.visibility === "list" ? "live-catalog" : "live-hidden";
|
|
59
|
+
models.push({
|
|
60
|
+
id: slug,
|
|
61
|
+
label: typeof record.display_name === "string" ? record.display_name : undefined,
|
|
62
|
+
description: shortDescription(record.description),
|
|
63
|
+
origin,
|
|
64
|
+
isDefault: false,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
return { models, defaultModel: null, unmapped };
|
|
68
|
+
}
|
|
69
|
+
function parseGrokModelsText(raw) {
|
|
70
|
+
const models = [];
|
|
71
|
+
const unmapped = [];
|
|
72
|
+
let defaultModel = null;
|
|
73
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
74
|
+
const trimmed = line.trim();
|
|
75
|
+
if (trimmed.length === 0)
|
|
76
|
+
continue;
|
|
77
|
+
const defaultMatch = /^default model:\s*(\S+)/i.exec(trimmed);
|
|
78
|
+
if (defaultMatch) {
|
|
79
|
+
defaultModel = defaultMatch[1];
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const bullet = /^[*-]\s+(\S+)(\s*\(default\))?\s*$/.exec(trimmed);
|
|
83
|
+
if (!bullet)
|
|
84
|
+
continue;
|
|
85
|
+
const id = bullet[1];
|
|
86
|
+
if (!isPlausibleModelId(id)) {
|
|
87
|
+
unmapped.push({
|
|
88
|
+
kind: "model",
|
|
89
|
+
raw: trimmed,
|
|
90
|
+
checksum: checksumText(trimmed),
|
|
91
|
+
reason: "grok models bullet line has a non-id token",
|
|
92
|
+
});
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const isDefault = Boolean(bullet[2]) || id === defaultModel;
|
|
96
|
+
if (isDefault)
|
|
97
|
+
defaultModel = id;
|
|
98
|
+
models.push({ id, origin: "live-catalog", isDefault });
|
|
99
|
+
}
|
|
100
|
+
return { models, defaultModel, unmapped };
|
|
101
|
+
}
|
|
102
|
+
function isPlausibleModelLabel(label) {
|
|
103
|
+
if (label.length < 2 || label.length > 60)
|
|
104
|
+
return false;
|
|
105
|
+
if (label.endsWith(":"))
|
|
106
|
+
return false;
|
|
107
|
+
if (/^[-*=_#>|]+/.test(label))
|
|
108
|
+
return false;
|
|
109
|
+
if (/\s{2,}/.test(label))
|
|
110
|
+
return false;
|
|
111
|
+
if (/https?:\/\//i.test(label))
|
|
112
|
+
return false;
|
|
113
|
+
if ((label.match(/\s/g)?.length ?? 0) > 8)
|
|
114
|
+
return false;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
function parseAgyModelsText(raw) {
|
|
118
|
+
const models = [];
|
|
119
|
+
const unmapped = [];
|
|
120
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
121
|
+
const label = line.trim();
|
|
122
|
+
if (label.length === 0)
|
|
123
|
+
continue;
|
|
124
|
+
if (!isPlausibleModelLabel(label)) {
|
|
125
|
+
unmapped.push({
|
|
126
|
+
kind: "model",
|
|
127
|
+
raw: label.slice(0, 120),
|
|
128
|
+
checksum: checksumText(label),
|
|
129
|
+
reason: "agy models line is not a plausible model label",
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
models.push({ id: label, label, origin: "account-label", isDefault: false });
|
|
134
|
+
}
|
|
135
|
+
return { models, defaultModel: null, unmapped };
|
|
136
|
+
}
|
|
137
|
+
function registryOrigin(source) {
|
|
138
|
+
switch (source) {
|
|
139
|
+
case "env":
|
|
140
|
+
return "env";
|
|
141
|
+
case "config":
|
|
142
|
+
return "config";
|
|
143
|
+
default:
|
|
144
|
+
return "curated-fallback";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function mergeRegistryModels(base, defaultModel, info) {
|
|
148
|
+
if (!info)
|
|
149
|
+
return { models: base, defaultModel };
|
|
150
|
+
const byId = new Map();
|
|
151
|
+
for (const model of base)
|
|
152
|
+
byId.set(model.id, model);
|
|
153
|
+
const resolvedDefault = defaultModel ?? info.defaultModel ?? null;
|
|
154
|
+
for (const [id, description] of Object.entries(info.models)) {
|
|
155
|
+
if (byId.has(id))
|
|
156
|
+
continue;
|
|
157
|
+
byId.set(id, {
|
|
158
|
+
id,
|
|
159
|
+
description: shortDescription(description),
|
|
160
|
+
origin: registryOrigin(info.modelMetadata?.[id]?.source),
|
|
161
|
+
isDefault: id === resolvedDefault,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
const models = [...byId.values()].map(model => ({
|
|
165
|
+
...model,
|
|
166
|
+
isDefault: model.id === resolvedDefault ? true : model.isDefault,
|
|
167
|
+
}));
|
|
168
|
+
return { models, defaultModel: resolvedDefault };
|
|
169
|
+
}
|
|
170
|
+
export function discoverProviderModels(def, set, options = {}) {
|
|
171
|
+
const md = def.discovery.modelDiscovery;
|
|
172
|
+
const raw = set.modelCatalog.raw ?? "";
|
|
173
|
+
let native;
|
|
174
|
+
let source;
|
|
175
|
+
switch (md.parse) {
|
|
176
|
+
case "codex-debug-json":
|
|
177
|
+
native = raw.trim().length > 0 ? parseCodexDebugJson(raw) : EMPTY_NATIVE;
|
|
178
|
+
source = "live-command";
|
|
179
|
+
break;
|
|
180
|
+
case "grok-models-text":
|
|
181
|
+
native = raw.trim().length > 0 ? parseGrokModelsText(raw) : EMPTY_NATIVE;
|
|
182
|
+
source = "live-command";
|
|
183
|
+
break;
|
|
184
|
+
case "agy-models-text":
|
|
185
|
+
native = raw.trim().length > 0 ? parseAgyModelsText(raw) : EMPTY_NATIVE;
|
|
186
|
+
source = "live-command";
|
|
187
|
+
break;
|
|
188
|
+
case "config-or-env":
|
|
189
|
+
native = EMPTY_NATIVE;
|
|
190
|
+
source = "config-or-env";
|
|
191
|
+
break;
|
|
192
|
+
case "curated-catalog":
|
|
193
|
+
native = EMPTY_NATIVE;
|
|
194
|
+
source = "curated-catalog";
|
|
195
|
+
break;
|
|
196
|
+
default:
|
|
197
|
+
return assertNever(md.parse, "ModelCatalogParseFormat");
|
|
198
|
+
}
|
|
199
|
+
const merged = mergeRegistryModels(native.models, native.defaultModel, options.registryInfo);
|
|
200
|
+
return {
|
|
201
|
+
providerId: def.id,
|
|
202
|
+
strategy: md.strategy,
|
|
203
|
+
parse: md.parse,
|
|
204
|
+
fallbackPolicy: md.fallbackPolicy,
|
|
205
|
+
source,
|
|
206
|
+
defaultModel: merged.defaultModel ? scrubString(merged.defaultModel) : merged.defaultModel,
|
|
207
|
+
models: merged.models.map(scrubModel),
|
|
208
|
+
facts: md.facts,
|
|
209
|
+
configSources: md.configSources,
|
|
210
|
+
catalogChecksum: set.checksums.modelCatalog,
|
|
211
|
+
discoveredUnmapped: native.unmapped.map(scrubUnmapped),
|
|
212
|
+
evidence: scrubString(md.evidence),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
function scrubModel(model) {
|
|
216
|
+
return {
|
|
217
|
+
...model,
|
|
218
|
+
id: scrubString(model.id),
|
|
219
|
+
...(model.label !== undefined ? { label: scrubString(model.label) } : {}),
|
|
220
|
+
...(model.description !== undefined ? { description: scrubString(model.description) } : {}),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
function scrubUnmapped(unmapped) {
|
|
224
|
+
return {
|
|
225
|
+
...unmapped,
|
|
226
|
+
raw: scrubString(unmapped.raw),
|
|
227
|
+
reason: scrubString(unmapped.reason),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ProviderMetadataAbsentField = "sessionId" | "stopReason" | "usage";
|
|
2
|
+
export interface ProviderOutputMetadata {
|
|
3
|
+
sessionId?: string;
|
|
4
|
+
stopReason?: string;
|
|
5
|
+
absentFields: readonly ProviderMetadataAbsentField[];
|
|
6
|
+
}
|
|
7
|
+
export declare function extractProviderOutputMetadata(cli: string, stdout: string, outputFormat: string | undefined): ProviderOutputMetadata;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { parseStreamJson } from "./stream-json-parser.js";
|
|
2
|
+
import { parseCodexJsonStream } from "./codex-json-parser.js";
|
|
3
|
+
import { parseGeminiJson, parseGeminiStreamJson } from "./gemini-json-parser.js";
|
|
4
|
+
import { parseGrokOutput } from "./grok-json-parser.js";
|
|
5
|
+
export function extractProviderOutputMetadata(cli, stdout, outputFormat) {
|
|
6
|
+
switch (cli) {
|
|
7
|
+
case "claude": {
|
|
8
|
+
const parsed = parseStreamJson(stdout);
|
|
9
|
+
const out = { absentFields: [] };
|
|
10
|
+
if (parsed.sessionId)
|
|
11
|
+
out.sessionId = parsed.sessionId;
|
|
12
|
+
if (parsed.stopReason)
|
|
13
|
+
out.stopReason = parsed.stopReason;
|
|
14
|
+
if (!out.sessionId)
|
|
15
|
+
out.absentFields.push("sessionId");
|
|
16
|
+
if (!out.stopReason)
|
|
17
|
+
out.absentFields.push("stopReason");
|
|
18
|
+
return out;
|
|
19
|
+
}
|
|
20
|
+
case "codex": {
|
|
21
|
+
const parsed = parseCodexJsonStream(stdout);
|
|
22
|
+
const out = { absentFields: [] };
|
|
23
|
+
if (parsed.threadId)
|
|
24
|
+
out.sessionId = parsed.threadId;
|
|
25
|
+
if (parsed.stopReason)
|
|
26
|
+
out.stopReason = parsed.stopReason;
|
|
27
|
+
if (!out.sessionId)
|
|
28
|
+
out.absentFields.push("sessionId");
|
|
29
|
+
if (!out.stopReason)
|
|
30
|
+
out.absentFields.push("stopReason");
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
case "gemini": {
|
|
34
|
+
const parsed = outputFormat === "stream-json"
|
|
35
|
+
? parseGeminiStreamJson(stdout)
|
|
36
|
+
: outputFormat === "json"
|
|
37
|
+
? parseGeminiJson(stdout)
|
|
38
|
+
: null;
|
|
39
|
+
const out = { absentFields: [] };
|
|
40
|
+
if (parsed?.sessionId)
|
|
41
|
+
out.sessionId = parsed.sessionId;
|
|
42
|
+
if (parsed?.stopReason)
|
|
43
|
+
out.stopReason = parsed.stopReason;
|
|
44
|
+
if (!out.sessionId)
|
|
45
|
+
out.absentFields.push("sessionId");
|
|
46
|
+
if (!out.stopReason)
|
|
47
|
+
out.absentFields.push("stopReason");
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
case "grok": {
|
|
51
|
+
const parsed = parseGrokOutput(outputFormat, stdout);
|
|
52
|
+
const out = { absentFields: ["usage"] };
|
|
53
|
+
if (parsed?.sessionId)
|
|
54
|
+
out.sessionId = parsed.sessionId;
|
|
55
|
+
if (parsed?.stopReason)
|
|
56
|
+
out.stopReason = parsed.stopReason;
|
|
57
|
+
if (!out.sessionId)
|
|
58
|
+
out.absentFields.push("sessionId");
|
|
59
|
+
if (!out.stopReason)
|
|
60
|
+
out.absentFields.push("stopReason");
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
case "mistral":
|
|
64
|
+
return { absentFields: ["sessionId", "stopReason"] };
|
|
65
|
+
default:
|
|
66
|
+
return { absentFields: ["sessionId", "stopReason"] };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { CliType } from "./provider-definitions.js";
|
|
2
|
+
import type { DiscoveredCapabilitySet } from "./provider-capability-discovery.js";
|
|
3
|
+
import { type DiscoveredUnmapped, type ParsedArgvPlacement, type ParsedFlagSafety } from "./provider-help-parser.js";
|
|
4
|
+
export type RequestFieldZodType = "boolean" | "string" | "number" | "enum" | "string-array";
|
|
5
|
+
export interface RequestFieldDescriptor {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
readonly flag: string;
|
|
8
|
+
readonly aliases: readonly string[];
|
|
9
|
+
readonly zodType: RequestFieldZodType;
|
|
10
|
+
readonly enumValues: readonly string[];
|
|
11
|
+
readonly repeatable: boolean;
|
|
12
|
+
readonly argvPlacement: ParsedArgvPlacement;
|
|
13
|
+
readonly safety: ParsedFlagSafety;
|
|
14
|
+
readonly description: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ProviderSchemaProjection {
|
|
17
|
+
readonly providerId: CliType;
|
|
18
|
+
readonly fields: readonly RequestFieldDescriptor[];
|
|
19
|
+
readonly discoveredUnmapped: readonly DiscoveredUnmapped[];
|
|
20
|
+
}
|
|
21
|
+
export declare function flagToFieldName(flag: string): string;
|
|
22
|
+
export declare function buildProviderSchema(set: DiscoveredCapabilitySet): ProviderSchemaProjection;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { checksumText, } from "./provider-help-parser.js";
|
|
2
|
+
export function flagToFieldName(flag) {
|
|
3
|
+
const stripped = flag.replace(/^-+/, "");
|
|
4
|
+
return stripped
|
|
5
|
+
.split(/[-_]/)
|
|
6
|
+
.filter(Boolean)
|
|
7
|
+
.map((part, index) => (index === 0 ? part : part.charAt(0).toUpperCase() + part.slice(1)))
|
|
8
|
+
.join("");
|
|
9
|
+
}
|
|
10
|
+
function zodTypeForFlag(flag) {
|
|
11
|
+
if (flag.arity === "none")
|
|
12
|
+
return "boolean";
|
|
13
|
+
if (flag.valueType === "unknown")
|
|
14
|
+
return null;
|
|
15
|
+
if (flag.valueType === "enum")
|
|
16
|
+
return flag.arity === "many" ? "string-array" : "enum";
|
|
17
|
+
if (flag.arity === "many" || flag.repeatable)
|
|
18
|
+
return "string-array";
|
|
19
|
+
if (flag.valueType === "number")
|
|
20
|
+
return "number";
|
|
21
|
+
return "string";
|
|
22
|
+
}
|
|
23
|
+
export function buildProviderSchema(set) {
|
|
24
|
+
const fields = [];
|
|
25
|
+
const unmapped = [...set.discoveredUnmapped];
|
|
26
|
+
const seen = new Set();
|
|
27
|
+
for (const flag of set.rootHelp.flags) {
|
|
28
|
+
const zodType = zodTypeForFlag(flag);
|
|
29
|
+
if (zodType === null) {
|
|
30
|
+
unmapped.push({
|
|
31
|
+
kind: "flag",
|
|
32
|
+
raw: `${flag.name} ${flag.description}`.trim(),
|
|
33
|
+
checksum: checksumText(`${flag.name} ${flag.description}`.trim()),
|
|
34
|
+
reason: "flag takes a value but its type could not be inferred (arity/type/safety incomplete); exposed as discovered-unmapped rather than a request field",
|
|
35
|
+
});
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
const name = flagToFieldName(flag.name);
|
|
39
|
+
if (name.length === 0 || seen.has(name))
|
|
40
|
+
continue;
|
|
41
|
+
seen.add(name);
|
|
42
|
+
fields.push({
|
|
43
|
+
name,
|
|
44
|
+
flag: flag.name,
|
|
45
|
+
aliases: flag.aliases,
|
|
46
|
+
zodType,
|
|
47
|
+
enumValues: flag.enumValues,
|
|
48
|
+
repeatable: flag.repeatable,
|
|
49
|
+
argvPlacement: flag.argvPlacement,
|
|
50
|
+
safety: flag.safety,
|
|
51
|
+
description: flag.description,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return { providerId: set.providerId, fields, discoveredUnmapped: unmapped };
|
|
55
|
+
}
|