@bilalimamoglu/sift 0.1.0 → 0.2.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/README.md +66 -3
- package/dist/cli.js +234 -18
- package/dist/index.d.ts +5 -1
- package/dist/index.js +189 -10
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -48,8 +48,8 @@ Set credentials once in your shell:
|
|
|
48
48
|
|
|
49
49
|
```bash
|
|
50
50
|
export SIFT_BASE_URL=https://api.openai.com/v1
|
|
51
|
-
export SIFT_API_KEY=your_api_key
|
|
52
51
|
export SIFT_MODEL=gpt-4.1-mini
|
|
52
|
+
export OPENAI_API_KEY=your_openai_api_key
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
Or write them to a config file:
|
|
@@ -58,7 +58,20 @@ Or write them to a config file:
|
|
|
58
58
|
sift config init
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
|
|
61
|
+
For the default OpenAI-compatible setup, `OPENAI_API_KEY` works directly. If you point `SIFT_BASE_URL` at a different compatible endpoint, use that provider's native key when `sift` recognizes the endpoint, or set the generic fallback env:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
export SIFT_PROVIDER_API_KEY=your_provider_api_key
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`SIFT_PROVIDER_API_KEY` is the generic wrapper env for custom or self-hosted compatible endpoints. Today's `openai-compatible` mode stays generic and does not imply OpenAI ownership.
|
|
68
|
+
|
|
69
|
+
Known native env fallbacks for recognized compatible endpoints:
|
|
70
|
+
|
|
71
|
+
- `OPENAI_API_KEY` for `https://api.openai.com/v1`
|
|
72
|
+
- `OPENROUTER_API_KEY` for `https://openrouter.ai/api/v1`
|
|
73
|
+
- `TOGETHER_API_KEY` for `https://api.together.xyz/v1`
|
|
74
|
+
- `GROQ_API_KEY` for `https://api.groq.com/openai/v1`
|
|
62
75
|
|
|
63
76
|
## Quick start
|
|
64
77
|
|
|
@@ -77,6 +90,7 @@ sift exec --preset infra-risk -- terraform plan
|
|
|
77
90
|
sift exec "did tests pass?" -- pytest
|
|
78
91
|
sift exec "what changed?" -- git diff
|
|
79
92
|
sift exec --preset infra-risk -- terraform plan
|
|
93
|
+
sift exec --dry-run "what changed?" -- git diff
|
|
80
94
|
```
|
|
81
95
|
|
|
82
96
|
What happens:
|
|
@@ -88,6 +102,8 @@ What happens:
|
|
|
88
102
|
5. It prints a short answer or JSON.
|
|
89
103
|
6. It preserves the wrapped command's exit code.
|
|
90
104
|
|
|
105
|
+
Use `--dry-run` to inspect the reduced input and prompt without calling the provider.
|
|
106
|
+
|
|
91
107
|
## Pipe mode
|
|
92
108
|
|
|
93
109
|
If the output already exists in a pipeline, pipe mode still works:
|
|
@@ -125,6 +141,20 @@ sift presets show audit-critical
|
|
|
125
141
|
|
|
126
142
|
Some built-in presets also use local heuristics before calling a model. For example, `infra-risk` can mark obvious destructive plans as `fail` without sending the whole decision to the model.
|
|
127
143
|
|
|
144
|
+
## JSON response format
|
|
145
|
+
|
|
146
|
+
When `format` resolves to JSON, `sift` can ask the provider for native JSON output.
|
|
147
|
+
|
|
148
|
+
- `auto`: enable native JSON mode only for known-safe endpoints such as `https://api.openai.com/v1`
|
|
149
|
+
- `on`: always send the native JSON response format request
|
|
150
|
+
- `off`: never send it
|
|
151
|
+
|
|
152
|
+
Example:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
sift exec --format json --json-response-format on "summarize this" -- some-command
|
|
156
|
+
```
|
|
157
|
+
|
|
128
158
|
## Config
|
|
129
159
|
|
|
130
160
|
Generate an example config:
|
|
@@ -150,7 +180,11 @@ Supported environment variables:
|
|
|
150
180
|
- `SIFT_PROVIDER`
|
|
151
181
|
- `SIFT_MODEL`
|
|
152
182
|
- `SIFT_BASE_URL`
|
|
153
|
-
- `
|
|
183
|
+
- `SIFT_PROVIDER_API_KEY`
|
|
184
|
+
- `OPENAI_API_KEY` for `https://api.openai.com/v1`
|
|
185
|
+
- `OPENROUTER_API_KEY` for `https://openrouter.ai/api/v1`
|
|
186
|
+
- `TOGETHER_API_KEY` for `https://api.together.xyz/v1`
|
|
187
|
+
- `GROQ_API_KEY` for `https://api.groq.com/openai/v1`
|
|
154
188
|
- `SIFT_MAX_CAPTURE_CHARS`
|
|
155
189
|
- `SIFT_TIMEOUT_MS`
|
|
156
190
|
- `SIFT_MAX_INPUT_CHARS`
|
|
@@ -198,6 +232,34 @@ sift presets list
|
|
|
198
232
|
sift presets show <name>
|
|
199
233
|
```
|
|
200
234
|
|
|
235
|
+
## Releasing
|
|
236
|
+
|
|
237
|
+
`sift` uses a manual GitHub Actions release workflow with npm trusted publishing.
|
|
238
|
+
|
|
239
|
+
Before the first release:
|
|
240
|
+
|
|
241
|
+
1. configure npm trusted publishing for `@bilalimamoglu/sift`
|
|
242
|
+
2. point it at `bilalimamoglu/sift`
|
|
243
|
+
3. use the workflow filename `release.yml`
|
|
244
|
+
4. set the GitHub Actions environment name to `release`
|
|
245
|
+
|
|
246
|
+
For each release:
|
|
247
|
+
|
|
248
|
+
1. update `package.json` to the target version
|
|
249
|
+
2. merge the final release commit to `main`
|
|
250
|
+
3. open GitHub Actions and run the `release` workflow manually
|
|
251
|
+
|
|
252
|
+
The workflow will:
|
|
253
|
+
|
|
254
|
+
1. install dependencies
|
|
255
|
+
2. typecheck, test, and build
|
|
256
|
+
3. pack and smoke-test the tarball
|
|
257
|
+
4. publish to npm
|
|
258
|
+
5. create and push the `vX.Y.Z` tag
|
|
259
|
+
6. create a GitHub Release
|
|
260
|
+
|
|
261
|
+
`release.yml` uses OIDC trusted publishing, so it does not require an `NPM_TOKEN`.
|
|
262
|
+
|
|
201
263
|
## Using it with Codex
|
|
202
264
|
|
|
203
265
|
`sift` does not install itself into Codex. The normal setup is:
|
|
@@ -226,6 +288,7 @@ That gives the agent a simple habit:
|
|
|
226
288
|
- Redaction is optional and regex-based.
|
|
227
289
|
- Redaction is off by default. If command output may contain secrets, enable `--redact` or set it in config before sending output to a provider.
|
|
228
290
|
- Built-in JSON and verdict flows return strict error objects on provider/model failure.
|
|
291
|
+
- Retriable provider failures such as `429`, timeouts, and `5xx` responses are retried once before falling back.
|
|
229
292
|
- `sift exec` detects simple prompt-like output such as `[y/N]` or `password:` and skips reduction instead of guessing.
|
|
230
293
|
- Pipe mode does not preserve upstream shell pipeline failures; use `set -o pipefail` if you need that behavior.
|
|
231
294
|
- `sift exec` mirrors the wrapped command's exit code.
|
package/dist/cli.js
CHANGED
|
@@ -55,6 +55,7 @@ var defaultConfig = {
|
|
|
55
55
|
model: "gpt-4.1-mini",
|
|
56
56
|
baseUrl: "https://api.openai.com/v1",
|
|
57
57
|
apiKey: "",
|
|
58
|
+
jsonResponseFormat: "auto",
|
|
58
59
|
timeoutMs: 2e4,
|
|
59
60
|
temperature: 0.1,
|
|
60
61
|
maxOutputTokens: 220
|
|
@@ -108,6 +109,70 @@ var defaultConfig = {
|
|
|
108
109
|
}
|
|
109
110
|
};
|
|
110
111
|
|
|
112
|
+
// src/config/provider-api-key.ts
|
|
113
|
+
var OPENAI_COMPATIBLE_BASE_URL_ENV = [
|
|
114
|
+
{ prefix: "https://api.openai.com/", envName: "OPENAI_API_KEY" },
|
|
115
|
+
{ prefix: "https://openrouter.ai/api/", envName: "OPENROUTER_API_KEY" },
|
|
116
|
+
{ prefix: "https://api.together.xyz/", envName: "TOGETHER_API_KEY" },
|
|
117
|
+
{ prefix: "https://api.groq.com/openai/", envName: "GROQ_API_KEY" }
|
|
118
|
+
];
|
|
119
|
+
var PROVIDER_API_KEY_ENV = {
|
|
120
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
121
|
+
claude: "ANTHROPIC_API_KEY",
|
|
122
|
+
groq: "GROQ_API_KEY",
|
|
123
|
+
openai: "OPENAI_API_KEY",
|
|
124
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
125
|
+
together: "TOGETHER_API_KEY"
|
|
126
|
+
};
|
|
127
|
+
function normalizeBaseUrl(baseUrl) {
|
|
128
|
+
if (!baseUrl) {
|
|
129
|
+
return void 0;
|
|
130
|
+
}
|
|
131
|
+
return `${baseUrl.replace(/\/+$/, "")}/`.toLowerCase();
|
|
132
|
+
}
|
|
133
|
+
function resolveCompatibleEnvName(baseUrl) {
|
|
134
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
135
|
+
if (!normalized) {
|
|
136
|
+
return void 0;
|
|
137
|
+
}
|
|
138
|
+
const match = OPENAI_COMPATIBLE_BASE_URL_ENV.find(
|
|
139
|
+
(entry) => normalized.startsWith(entry.prefix)
|
|
140
|
+
);
|
|
141
|
+
return match?.envName;
|
|
142
|
+
}
|
|
143
|
+
function resolveProviderApiKey(provider, baseUrl, env) {
|
|
144
|
+
if (env.SIFT_PROVIDER_API_KEY) {
|
|
145
|
+
return env.SIFT_PROVIDER_API_KEY;
|
|
146
|
+
}
|
|
147
|
+
if (provider === "openai-compatible") {
|
|
148
|
+
const envName2 = resolveCompatibleEnvName(baseUrl);
|
|
149
|
+
return envName2 ? env[envName2] : void 0;
|
|
150
|
+
}
|
|
151
|
+
if (!provider) {
|
|
152
|
+
return void 0;
|
|
153
|
+
}
|
|
154
|
+
const envName = PROVIDER_API_KEY_ENV[provider];
|
|
155
|
+
return envName ? env[envName] : void 0;
|
|
156
|
+
}
|
|
157
|
+
function getProviderApiKeyEnvNames(provider, baseUrl) {
|
|
158
|
+
const envNames = ["SIFT_PROVIDER_API_KEY"];
|
|
159
|
+
if (provider === "openai-compatible") {
|
|
160
|
+
const envName2 = resolveCompatibleEnvName(baseUrl);
|
|
161
|
+
if (envName2) {
|
|
162
|
+
envNames.push(envName2);
|
|
163
|
+
}
|
|
164
|
+
return envNames;
|
|
165
|
+
}
|
|
166
|
+
if (!provider) {
|
|
167
|
+
return envNames;
|
|
168
|
+
}
|
|
169
|
+
const envName = PROVIDER_API_KEY_ENV[provider];
|
|
170
|
+
if (envName) {
|
|
171
|
+
envNames.push(envName);
|
|
172
|
+
}
|
|
173
|
+
return envNames;
|
|
174
|
+
}
|
|
175
|
+
|
|
111
176
|
// src/config/schema.ts
|
|
112
177
|
import { z } from "zod";
|
|
113
178
|
var providerNameSchema = z.enum(["openai-compatible"]);
|
|
@@ -118,6 +183,7 @@ var outputFormatSchema = z.enum([
|
|
|
118
183
|
"verdict"
|
|
119
184
|
]);
|
|
120
185
|
var responseModeSchema = z.enum(["text", "json"]);
|
|
186
|
+
var jsonResponseFormatModeSchema = z.enum(["auto", "on", "off"]);
|
|
121
187
|
var promptPolicyNameSchema = z.enum([
|
|
122
188
|
"test-status",
|
|
123
189
|
"audit-critical",
|
|
@@ -131,6 +197,7 @@ var providerConfigSchema = z.object({
|
|
|
131
197
|
model: z.string().min(1),
|
|
132
198
|
baseUrl: z.string().url(),
|
|
133
199
|
apiKey: z.string().optional(),
|
|
200
|
+
jsonResponseFormat: jsonResponseFormatModeSchema,
|
|
134
201
|
timeoutMs: z.number().int().positive(),
|
|
135
202
|
temperature: z.number().min(0).max(2),
|
|
136
203
|
maxOutputTokens: z.number().int().positive()
|
|
@@ -184,14 +251,25 @@ function mergeDefined(base, override) {
|
|
|
184
251
|
}
|
|
185
252
|
return result;
|
|
186
253
|
}
|
|
187
|
-
function
|
|
254
|
+
function stripApiKey(overrides) {
|
|
255
|
+
if (!overrides?.provider || overrides.provider.apiKey === void 0) {
|
|
256
|
+
return overrides;
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
...overrides,
|
|
260
|
+
provider: {
|
|
261
|
+
...overrides.provider,
|
|
262
|
+
apiKey: void 0
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function buildNonCredentialEnvOverrides(env) {
|
|
188
267
|
const overrides = {};
|
|
189
|
-
if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.
|
|
268
|
+
if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.SIFT_TIMEOUT_MS) {
|
|
190
269
|
overrides.provider = {
|
|
191
270
|
provider: env.SIFT_PROVIDER,
|
|
192
271
|
model: env.SIFT_MODEL,
|
|
193
272
|
baseUrl: env.SIFT_BASE_URL,
|
|
194
|
-
apiKey: env.SIFT_API_KEY,
|
|
195
273
|
timeoutMs: env.SIFT_TIMEOUT_MS ? Number(env.SIFT_TIMEOUT_MS) : void 0
|
|
196
274
|
};
|
|
197
275
|
}
|
|
@@ -203,12 +281,40 @@ function buildEnvOverrides(env) {
|
|
|
203
281
|
}
|
|
204
282
|
return overrides;
|
|
205
283
|
}
|
|
284
|
+
function buildCredentialEnvOverrides(env, context) {
|
|
285
|
+
const apiKey = resolveProviderApiKey(context.provider, context.baseUrl, env);
|
|
286
|
+
if (apiKey === void 0) {
|
|
287
|
+
return {};
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
provider: {
|
|
291
|
+
apiKey
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
206
295
|
function resolveConfig(options = {}) {
|
|
207
296
|
const env = options.env ?? process.env;
|
|
208
297
|
const fileConfig = loadRawConfig(options.configPath);
|
|
209
|
-
const
|
|
298
|
+
const nonCredentialEnvConfig = buildNonCredentialEnvOverrides(env);
|
|
299
|
+
const contextConfig = mergeDefined(
|
|
300
|
+
mergeDefined(
|
|
301
|
+
mergeDefined(defaultConfig, fileConfig),
|
|
302
|
+
nonCredentialEnvConfig
|
|
303
|
+
),
|
|
304
|
+
stripApiKey(options.cliOverrides) ?? {}
|
|
305
|
+
);
|
|
306
|
+
const credentialEnvConfig = buildCredentialEnvOverrides(env, {
|
|
307
|
+
provider: contextConfig.provider.provider,
|
|
308
|
+
baseUrl: contextConfig.provider.baseUrl
|
|
309
|
+
});
|
|
210
310
|
const merged = mergeDefined(
|
|
211
|
-
mergeDefined(
|
|
311
|
+
mergeDefined(
|
|
312
|
+
mergeDefined(
|
|
313
|
+
mergeDefined(defaultConfig, fileConfig),
|
|
314
|
+
nonCredentialEnvConfig
|
|
315
|
+
),
|
|
316
|
+
credentialEnvConfig
|
|
317
|
+
),
|
|
212
318
|
options.cliOverrides ?? {}
|
|
213
319
|
);
|
|
214
320
|
return siftConfigSchema.parse(merged);
|
|
@@ -269,7 +375,7 @@ function configValidate(configPath) {
|
|
|
269
375
|
});
|
|
270
376
|
const resolvedPath = findConfigPath(configPath);
|
|
271
377
|
process.stdout.write(
|
|
272
|
-
`
|
|
378
|
+
`Resolved config is valid${resolvedPath ? ` (${resolvedPath})` : " (using defaults)"}.
|
|
273
379
|
`
|
|
274
380
|
);
|
|
275
381
|
}
|
|
@@ -278,6 +384,7 @@ function configValidate(configPath) {
|
|
|
278
384
|
function runDoctor(config) {
|
|
279
385
|
const lines = [
|
|
280
386
|
"sift doctor",
|
|
387
|
+
"mode: local config completeness check",
|
|
281
388
|
`provider: ${config.provider.provider}`,
|
|
282
389
|
`model: ${config.provider.model}`,
|
|
283
390
|
`baseUrl: ${config.provider.baseUrl}`,
|
|
@@ -297,6 +404,12 @@ function runDoctor(config) {
|
|
|
297
404
|
}
|
|
298
405
|
if (config.provider.provider === "openai-compatible" && !config.provider.apiKey) {
|
|
299
406
|
problems.push("Missing provider.apiKey");
|
|
407
|
+
problems.push(
|
|
408
|
+
`Set one of: ${getProviderApiKeyEnvNames(
|
|
409
|
+
config.provider.provider,
|
|
410
|
+
config.provider.baseUrl
|
|
411
|
+
).join(", ")}`
|
|
412
|
+
);
|
|
300
413
|
}
|
|
301
414
|
if (problems.length > 0) {
|
|
302
415
|
process.stderr.write(`${problems.join("\n")}
|
|
@@ -335,6 +448,15 @@ import pc2 from "picocolors";
|
|
|
335
448
|
import pc from "picocolors";
|
|
336
449
|
|
|
337
450
|
// src/providers/openaiCompatible.ts
|
|
451
|
+
function supportsNativeJsonResponseFormat(baseUrl, mode) {
|
|
452
|
+
if (mode === "off") {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
455
|
+
if (mode === "on") {
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
return /^https:\/\/api\.openai\.com(?:\/|$)/i.test(baseUrl);
|
|
459
|
+
}
|
|
338
460
|
function extractMessageText(payload) {
|
|
339
461
|
const content = payload?.choices?.[0]?.message?.content;
|
|
340
462
|
if (typeof content === "string") {
|
|
@@ -369,6 +491,7 @@ var OpenAICompatibleProvider = class {
|
|
|
369
491
|
model: input.model,
|
|
370
492
|
temperature: input.temperature,
|
|
371
493
|
max_tokens: input.maxOutputTokens,
|
|
494
|
+
...input.responseMode === "json" && supportsNativeJsonResponseFormat(this.baseUrl, input.jsonResponseFormat) ? { response_format: { type: "json_object" } } : {},
|
|
372
495
|
messages: [
|
|
373
496
|
{
|
|
374
497
|
role: "system",
|
|
@@ -879,6 +1002,7 @@ function prepareInput(raw, config) {
|
|
|
879
1002
|
}
|
|
880
1003
|
|
|
881
1004
|
// src/core/run.ts
|
|
1005
|
+
var RETRY_DELAY_MS = 300;
|
|
882
1006
|
function normalizeOutput(text, responseMode) {
|
|
883
1007
|
if (responseMode !== "json") {
|
|
884
1008
|
return text.trim();
|
|
@@ -890,6 +1014,68 @@ function normalizeOutput(text, responseMode) {
|
|
|
890
1014
|
throw new Error("Provider returned invalid JSON");
|
|
891
1015
|
}
|
|
892
1016
|
}
|
|
1017
|
+
function buildDryRunOutput(args) {
|
|
1018
|
+
return JSON.stringify(
|
|
1019
|
+
{
|
|
1020
|
+
status: "dry-run",
|
|
1021
|
+
strategy: args.heuristicOutput ? "heuristic" : "provider",
|
|
1022
|
+
provider: {
|
|
1023
|
+
name: args.providerName,
|
|
1024
|
+
model: args.request.config.provider.model,
|
|
1025
|
+
baseUrl: args.request.config.provider.baseUrl,
|
|
1026
|
+
jsonResponseFormat: args.request.config.provider.jsonResponseFormat
|
|
1027
|
+
},
|
|
1028
|
+
question: args.request.question,
|
|
1029
|
+
format: args.request.format,
|
|
1030
|
+
responseMode: args.responseMode,
|
|
1031
|
+
policy: args.request.policyName ?? null,
|
|
1032
|
+
heuristicOutput: args.heuristicOutput ?? null,
|
|
1033
|
+
input: {
|
|
1034
|
+
originalLength: args.prepared.meta.originalLength,
|
|
1035
|
+
finalLength: args.prepared.meta.finalLength,
|
|
1036
|
+
redactionApplied: args.prepared.meta.redactionApplied,
|
|
1037
|
+
truncatedApplied: args.prepared.meta.truncatedApplied,
|
|
1038
|
+
text: args.prepared.truncated
|
|
1039
|
+
},
|
|
1040
|
+
prompt: args.prompt
|
|
1041
|
+
},
|
|
1042
|
+
null,
|
|
1043
|
+
2
|
|
1044
|
+
);
|
|
1045
|
+
}
|
|
1046
|
+
async function delay(ms) {
|
|
1047
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1048
|
+
}
|
|
1049
|
+
async function generateWithRetry(args) {
|
|
1050
|
+
let lastError;
|
|
1051
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1052
|
+
try {
|
|
1053
|
+
return await args.provider.generate({
|
|
1054
|
+
model: args.request.config.provider.model,
|
|
1055
|
+
prompt: args.prompt,
|
|
1056
|
+
temperature: args.request.config.provider.temperature,
|
|
1057
|
+
maxOutputTokens: args.request.config.provider.maxOutputTokens,
|
|
1058
|
+
timeoutMs: args.request.config.provider.timeoutMs,
|
|
1059
|
+
responseMode: args.responseMode,
|
|
1060
|
+
jsonResponseFormat: args.request.config.provider.jsonResponseFormat
|
|
1061
|
+
});
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
lastError = error;
|
|
1064
|
+
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
1065
|
+
if (attempt > 0 || !isRetriableReason(reason)) {
|
|
1066
|
+
throw error;
|
|
1067
|
+
}
|
|
1068
|
+
if (args.request.config.runtime.verbose) {
|
|
1069
|
+
process.stderr.write(
|
|
1070
|
+
`${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
|
|
1071
|
+
`
|
|
1072
|
+
);
|
|
1073
|
+
}
|
|
1074
|
+
await delay(RETRY_DELAY_MS);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
throw lastError instanceof Error ? lastError : new Error("unknown_error");
|
|
1078
|
+
}
|
|
893
1079
|
async function runSift(request) {
|
|
894
1080
|
const prepared = prepareInput(request.stdin, request.config.input);
|
|
895
1081
|
const { prompt, responseMode } = buildPrompt({
|
|
@@ -915,15 +1101,33 @@ async function runSift(request) {
|
|
|
915
1101
|
process.stderr.write(`${pc.dim("sift")} heuristic=${request.policyName}
|
|
916
1102
|
`);
|
|
917
1103
|
}
|
|
1104
|
+
if (request.dryRun) {
|
|
1105
|
+
return buildDryRunOutput({
|
|
1106
|
+
request,
|
|
1107
|
+
providerName: provider.name,
|
|
1108
|
+
prompt,
|
|
1109
|
+
responseMode,
|
|
1110
|
+
prepared,
|
|
1111
|
+
heuristicOutput
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
918
1114
|
return heuristicOutput;
|
|
919
1115
|
}
|
|
1116
|
+
if (request.dryRun) {
|
|
1117
|
+
return buildDryRunOutput({
|
|
1118
|
+
request,
|
|
1119
|
+
providerName: provider.name,
|
|
1120
|
+
prompt,
|
|
1121
|
+
responseMode,
|
|
1122
|
+
prepared,
|
|
1123
|
+
heuristicOutput: null
|
|
1124
|
+
});
|
|
1125
|
+
}
|
|
920
1126
|
try {
|
|
921
|
-
const result = await
|
|
922
|
-
|
|
1127
|
+
const result = await generateWithRetry({
|
|
1128
|
+
provider,
|
|
1129
|
+
request,
|
|
923
1130
|
prompt,
|
|
924
|
-
temperature: request.config.provider.temperature,
|
|
925
|
-
maxOutputTokens: request.config.provider.maxOutputTokens,
|
|
926
|
-
timeoutMs: request.config.provider.timeoutMs,
|
|
927
1131
|
responseMode
|
|
928
1132
|
});
|
|
929
1133
|
if (looksLikeRejectedModelOutput({
|
|
@@ -1138,12 +1342,13 @@ function toNumber(value) {
|
|
|
1138
1342
|
}
|
|
1139
1343
|
function buildCliOverrides(options) {
|
|
1140
1344
|
const overrides = {};
|
|
1141
|
-
if (options.provider !== void 0 || options.model !== void 0 || options.baseUrl !== void 0 || options.apiKey !== void 0 || options.timeoutMs !== void 0) {
|
|
1345
|
+
if (options.provider !== void 0 || options.model !== void 0 || options.baseUrl !== void 0 || options.apiKey !== void 0 || options.jsonResponseFormat !== void 0 || options.timeoutMs !== void 0) {
|
|
1142
1346
|
overrides.provider = {
|
|
1143
1347
|
provider: options.provider,
|
|
1144
1348
|
model: options.model,
|
|
1145
1349
|
baseUrl: options.baseUrl,
|
|
1146
1350
|
apiKey: options.apiKey,
|
|
1351
|
+
jsonResponseFormat: options.jsonResponseFormat,
|
|
1147
1352
|
timeoutMs: toNumber(options.timeoutMs)
|
|
1148
1353
|
};
|
|
1149
1354
|
}
|
|
@@ -1167,7 +1372,13 @@ function buildCliOverrides(options) {
|
|
|
1167
1372
|
return overrides;
|
|
1168
1373
|
}
|
|
1169
1374
|
function applySharedOptions(command) {
|
|
1170
|
-
return command.option("--provider <provider>", "Provider: openai-compatible").option("--model <model>", "Model name").option("--base-url <url>", "Provider base URL").option(
|
|
1375
|
+
return command.option("--provider <provider>", "Provider: openai-compatible").option("--model <model>", "Model name").option("--base-url <url>", "Provider base URL").option(
|
|
1376
|
+
"--api-key <key>",
|
|
1377
|
+
"Provider API key (or set SIFT_PROVIDER_API_KEY; OPENAI_API_KEY also works for api.openai.com)"
|
|
1378
|
+
).option(
|
|
1379
|
+
"--json-response-format <mode>",
|
|
1380
|
+
"JSON response format mode: auto | on | off"
|
|
1381
|
+
).option("--timeout-ms <ms>", "Request timeout in milliseconds").option("--format <format>", "brief | bullets | json | verdict").option("--max-capture-chars <n>", "Maximum raw child output chars kept in memory").option("--max-input-chars <n>", "Maximum input chars sent to the model").option("--head-chars <n>", "Head chars to preserve during truncation").option("--tail-chars <n>", "Tail chars to preserve during truncation").option("--strip-ansi", "Force ANSI stripping").option("--redact", "Enable standard redaction").option("--redact-strict", "Enable strict redaction").option("--raw-fallback", "Enable raw fallback text output").option("--dry-run", "Show the reduced input and prompt without calling the provider").option("--config <path>", "Path to config file").option("--verbose", "Enable verbose stderr logging");
|
|
1171
1382
|
}
|
|
1172
1383
|
async function executeRun(args) {
|
|
1173
1384
|
const config = resolveConfig({
|
|
@@ -1181,6 +1392,7 @@ async function executeRun(args) {
|
|
|
1181
1392
|
format: args.format,
|
|
1182
1393
|
stdin,
|
|
1183
1394
|
config,
|
|
1395
|
+
dryRun: Boolean(args.options.dryRun),
|
|
1184
1396
|
policyName: args.policyName,
|
|
1185
1397
|
outputContract: args.outputContract,
|
|
1186
1398
|
fallbackJson: args.fallbackJson
|
|
@@ -1213,6 +1425,7 @@ async function executeExec(args) {
|
|
|
1213
1425
|
question: args.question,
|
|
1214
1426
|
format: args.format,
|
|
1215
1427
|
config,
|
|
1428
|
+
dryRun: Boolean(args.options.dryRun),
|
|
1216
1429
|
policyName: args.policyName,
|
|
1217
1430
|
outputContract: args.outputContract,
|
|
1218
1431
|
fallbackJson: args.fallbackJson,
|
|
@@ -1221,7 +1434,7 @@ async function executeExec(args) {
|
|
|
1221
1434
|
}
|
|
1222
1435
|
applySharedOptions(
|
|
1223
1436
|
cli.command("preset <name>", "Run a named preset against piped CLI output")
|
|
1224
|
-
).action(async (name, options) => {
|
|
1437
|
+
).usage("preset <name> [options]").example("preset test-status < test-output.txt").action(async (name, options) => {
|
|
1225
1438
|
const config = resolveConfig({
|
|
1226
1439
|
configPath: options.config,
|
|
1227
1440
|
env: process.env,
|
|
@@ -1239,7 +1452,7 @@ applySharedOptions(
|
|
|
1239
1452
|
});
|
|
1240
1453
|
applySharedOptions(
|
|
1241
1454
|
cli.command("exec [question]", "Run a command and reduce its output").allowUnknownOptions()
|
|
1242
|
-
).option("--shell <command>", "Execute a shell command string instead of argv mode").option("--preset <name>", "Run a named preset in exec mode").action(async (question, options) => {
|
|
1455
|
+
).usage("exec [question] [options] -- <program> [args...]").example('exec "what changed?" -- git diff').example("exec --preset test-status -- pytest").example('exec --preset infra-risk --shell "terraform plan"').option("--shell <command>", "Execute a shell command string instead of argv mode").option("--preset <name>", "Run a named preset in exec mode").action(async (question, options) => {
|
|
1243
1456
|
if (question === "preset") {
|
|
1244
1457
|
throw new Error("Use 'sift exec --preset <name> -- <program> ...' instead.");
|
|
1245
1458
|
}
|
|
@@ -1276,7 +1489,10 @@ applySharedOptions(
|
|
|
1276
1489
|
options
|
|
1277
1490
|
});
|
|
1278
1491
|
});
|
|
1279
|
-
cli.command(
|
|
1492
|
+
cli.command(
|
|
1493
|
+
"config <action>",
|
|
1494
|
+
"Config commands: init | show | validate (show/validate use resolved runtime config)"
|
|
1495
|
+
).usage("config <init|show|validate> [options]").example("config init").example("config show").example("config validate --config ./sift.config.yaml").option("--path <path>", "Target config path for init").option("--config <path>", "Path to config file").option("--show-secrets", "Show secret values in config show").action((action, options) => {
|
|
1280
1496
|
if (action === "init") {
|
|
1281
1497
|
configInit(options.path);
|
|
1282
1498
|
return;
|
|
@@ -1294,14 +1510,14 @@ cli.command("config <action>", "Config commands: init | show | validate").option
|
|
|
1294
1510
|
}
|
|
1295
1511
|
throw new Error(`Unknown config action: ${action}`);
|
|
1296
1512
|
});
|
|
1297
|
-
cli.command("doctor", "
|
|
1513
|
+
cli.command("doctor", "Check local runtime config completeness").usage("doctor [options]").option("--config <path>", "Path to config file").action((options) => {
|
|
1298
1514
|
const config = resolveConfig({
|
|
1299
1515
|
configPath: options.config,
|
|
1300
1516
|
env: process.env
|
|
1301
1517
|
});
|
|
1302
1518
|
process.exitCode = runDoctor(config);
|
|
1303
1519
|
});
|
|
1304
|
-
cli.command("presets <action> [name]", "Preset commands: list | show").option("--config <path>", "Path to config file").option("--internal", "Show internal preset fields in presets show").action((action, name, options) => {
|
|
1520
|
+
cli.command("presets <action> [name]", "Preset commands: list | show").usage("presets <list|show> [name] [options]").example("presets list").example("presets show infra-risk").option("--config <path>", "Path to config file").option("--internal", "Show internal preset fields in presets show").action((action, name, options) => {
|
|
1305
1521
|
const config = resolveConfig({
|
|
1306
1522
|
configPath: options.config,
|
|
1307
1523
|
env: process.env
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
type ProviderName = "openai-compatible";
|
|
2
2
|
type OutputFormat = "brief" | "bullets" | "json" | "verdict";
|
|
3
3
|
type ResponseMode = "text" | "json";
|
|
4
|
+
type JsonResponseFormatMode = "auto" | "on" | "off";
|
|
4
5
|
type PromptPolicyName = "test-status" | "audit-critical" | "diff-summary" | "build-failure" | "log-errors" | "infra-risk";
|
|
5
6
|
interface ProviderConfig {
|
|
6
7
|
provider: ProviderName;
|
|
7
8
|
model: string;
|
|
8
9
|
baseUrl: string;
|
|
9
10
|
apiKey?: string;
|
|
11
|
+
jsonResponseFormat: JsonResponseFormatMode;
|
|
10
12
|
timeoutMs: number;
|
|
11
13
|
temperature: number;
|
|
12
14
|
maxOutputTokens: number;
|
|
@@ -50,6 +52,7 @@ interface GenerateInput {
|
|
|
50
52
|
maxOutputTokens: number;
|
|
51
53
|
timeoutMs: number;
|
|
52
54
|
responseMode: ResponseMode;
|
|
55
|
+
jsonResponseFormat: JsonResponseFormatMode;
|
|
53
56
|
}
|
|
54
57
|
interface UsageInfo {
|
|
55
58
|
inputTokens?: number;
|
|
@@ -66,6 +69,7 @@ interface RunRequest {
|
|
|
66
69
|
format: OutputFormat;
|
|
67
70
|
stdin: string;
|
|
68
71
|
config: SiftConfig;
|
|
72
|
+
dryRun?: boolean;
|
|
69
73
|
policyName?: PromptPolicyName;
|
|
70
74
|
outputContract?: string;
|
|
71
75
|
fallbackJson?: unknown;
|
|
@@ -103,4 +107,4 @@ interface ResolveOptions {
|
|
|
103
107
|
}
|
|
104
108
|
declare function resolveConfig(options?: ResolveOptions): SiftConfig;
|
|
105
109
|
|
|
106
|
-
export { type ExecRequest, type GenerateInput, type GenerateResult, type InputConfig, type LLMProvider, type OutputFormat, type PartialSiftConfig, type PreparedInput, type PresetDefinition, type PromptPolicyName, type ProviderConfig, type ProviderName, type ResolveOptions, type ResponseMode, type RunRequest, type RuntimeConfig, type SiftConfig, type UsageInfo, resolveConfig, runExec, runSift };
|
|
110
|
+
export { type ExecRequest, type GenerateInput, type GenerateResult, type InputConfig, type JsonResponseFormatMode, type LLMProvider, type OutputFormat, type PartialSiftConfig, type PreparedInput, type PresetDefinition, type PromptPolicyName, type ProviderConfig, type ProviderName, type ResolveOptions, type ResponseMode, type RunRequest, type RuntimeConfig, type SiftConfig, type UsageInfo, resolveConfig, runExec, runSift };
|
package/dist/index.js
CHANGED
|
@@ -20,6 +20,15 @@ var CAPTURE_OMITTED_MARKER = "\n...[captured output omitted]...\n";
|
|
|
20
20
|
import pc from "picocolors";
|
|
21
21
|
|
|
22
22
|
// src/providers/openaiCompatible.ts
|
|
23
|
+
function supportsNativeJsonResponseFormat(baseUrl, mode) {
|
|
24
|
+
if (mode === "off") {
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
if (mode === "on") {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
return /^https:\/\/api\.openai\.com(?:\/|$)/i.test(baseUrl);
|
|
31
|
+
}
|
|
23
32
|
function extractMessageText(payload) {
|
|
24
33
|
const content = payload?.choices?.[0]?.message?.content;
|
|
25
34
|
if (typeof content === "string") {
|
|
@@ -54,6 +63,7 @@ var OpenAICompatibleProvider = class {
|
|
|
54
63
|
model: input.model,
|
|
55
64
|
temperature: input.temperature,
|
|
56
65
|
max_tokens: input.maxOutputTokens,
|
|
66
|
+
...input.responseMode === "json" && supportsNativeJsonResponseFormat(this.baseUrl, input.jsonResponseFormat) ? { response_format: { type: "json_object" } } : {},
|
|
57
67
|
messages: [
|
|
58
68
|
{
|
|
59
69
|
role: "system",
|
|
@@ -564,6 +574,7 @@ function prepareInput(raw, config) {
|
|
|
564
574
|
}
|
|
565
575
|
|
|
566
576
|
// src/core/run.ts
|
|
577
|
+
var RETRY_DELAY_MS = 300;
|
|
567
578
|
function normalizeOutput(text, responseMode) {
|
|
568
579
|
if (responseMode !== "json") {
|
|
569
580
|
return text.trim();
|
|
@@ -575,6 +586,68 @@ function normalizeOutput(text, responseMode) {
|
|
|
575
586
|
throw new Error("Provider returned invalid JSON");
|
|
576
587
|
}
|
|
577
588
|
}
|
|
589
|
+
function buildDryRunOutput(args) {
|
|
590
|
+
return JSON.stringify(
|
|
591
|
+
{
|
|
592
|
+
status: "dry-run",
|
|
593
|
+
strategy: args.heuristicOutput ? "heuristic" : "provider",
|
|
594
|
+
provider: {
|
|
595
|
+
name: args.providerName,
|
|
596
|
+
model: args.request.config.provider.model,
|
|
597
|
+
baseUrl: args.request.config.provider.baseUrl,
|
|
598
|
+
jsonResponseFormat: args.request.config.provider.jsonResponseFormat
|
|
599
|
+
},
|
|
600
|
+
question: args.request.question,
|
|
601
|
+
format: args.request.format,
|
|
602
|
+
responseMode: args.responseMode,
|
|
603
|
+
policy: args.request.policyName ?? null,
|
|
604
|
+
heuristicOutput: args.heuristicOutput ?? null,
|
|
605
|
+
input: {
|
|
606
|
+
originalLength: args.prepared.meta.originalLength,
|
|
607
|
+
finalLength: args.prepared.meta.finalLength,
|
|
608
|
+
redactionApplied: args.prepared.meta.redactionApplied,
|
|
609
|
+
truncatedApplied: args.prepared.meta.truncatedApplied,
|
|
610
|
+
text: args.prepared.truncated
|
|
611
|
+
},
|
|
612
|
+
prompt: args.prompt
|
|
613
|
+
},
|
|
614
|
+
null,
|
|
615
|
+
2
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
async function delay(ms) {
|
|
619
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
620
|
+
}
|
|
621
|
+
async function generateWithRetry(args) {
|
|
622
|
+
let lastError;
|
|
623
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
624
|
+
try {
|
|
625
|
+
return await args.provider.generate({
|
|
626
|
+
model: args.request.config.provider.model,
|
|
627
|
+
prompt: args.prompt,
|
|
628
|
+
temperature: args.request.config.provider.temperature,
|
|
629
|
+
maxOutputTokens: args.request.config.provider.maxOutputTokens,
|
|
630
|
+
timeoutMs: args.request.config.provider.timeoutMs,
|
|
631
|
+
responseMode: args.responseMode,
|
|
632
|
+
jsonResponseFormat: args.request.config.provider.jsonResponseFormat
|
|
633
|
+
});
|
|
634
|
+
} catch (error) {
|
|
635
|
+
lastError = error;
|
|
636
|
+
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
637
|
+
if (attempt > 0 || !isRetriableReason(reason)) {
|
|
638
|
+
throw error;
|
|
639
|
+
}
|
|
640
|
+
if (args.request.config.runtime.verbose) {
|
|
641
|
+
process.stderr.write(
|
|
642
|
+
`${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
|
|
643
|
+
`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
await delay(RETRY_DELAY_MS);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
throw lastError instanceof Error ? lastError : new Error("unknown_error");
|
|
650
|
+
}
|
|
578
651
|
async function runSift(request) {
|
|
579
652
|
const prepared = prepareInput(request.stdin, request.config.input);
|
|
580
653
|
const { prompt, responseMode } = buildPrompt({
|
|
@@ -600,15 +673,33 @@ async function runSift(request) {
|
|
|
600
673
|
process.stderr.write(`${pc.dim("sift")} heuristic=${request.policyName}
|
|
601
674
|
`);
|
|
602
675
|
}
|
|
676
|
+
if (request.dryRun) {
|
|
677
|
+
return buildDryRunOutput({
|
|
678
|
+
request,
|
|
679
|
+
providerName: provider.name,
|
|
680
|
+
prompt,
|
|
681
|
+
responseMode,
|
|
682
|
+
prepared,
|
|
683
|
+
heuristicOutput
|
|
684
|
+
});
|
|
685
|
+
}
|
|
603
686
|
return heuristicOutput;
|
|
604
687
|
}
|
|
688
|
+
if (request.dryRun) {
|
|
689
|
+
return buildDryRunOutput({
|
|
690
|
+
request,
|
|
691
|
+
providerName: provider.name,
|
|
692
|
+
prompt,
|
|
693
|
+
responseMode,
|
|
694
|
+
prepared,
|
|
695
|
+
heuristicOutput: null
|
|
696
|
+
});
|
|
697
|
+
}
|
|
605
698
|
try {
|
|
606
|
-
const result = await
|
|
607
|
-
|
|
699
|
+
const result = await generateWithRetry({
|
|
700
|
+
provider,
|
|
701
|
+
request,
|
|
608
702
|
prompt,
|
|
609
|
-
temperature: request.config.provider.temperature,
|
|
610
|
-
maxOutputTokens: request.config.provider.maxOutputTokens,
|
|
611
|
-
timeoutMs: request.config.provider.timeoutMs,
|
|
612
703
|
responseMode
|
|
613
704
|
});
|
|
614
705
|
if (looksLikeRejectedModelOutput({
|
|
@@ -797,6 +888,7 @@ var defaultConfig = {
|
|
|
797
888
|
model: "gpt-4.1-mini",
|
|
798
889
|
baseUrl: "https://api.openai.com/v1",
|
|
799
890
|
apiKey: "",
|
|
891
|
+
jsonResponseFormat: "auto",
|
|
800
892
|
timeoutMs: 2e4,
|
|
801
893
|
temperature: 0.1,
|
|
802
894
|
maxOutputTokens: 220
|
|
@@ -878,6 +970,52 @@ function loadRawConfig(explicitPath) {
|
|
|
878
970
|
return YAML.parse(content) ?? {};
|
|
879
971
|
}
|
|
880
972
|
|
|
973
|
+
// src/config/provider-api-key.ts
|
|
974
|
+
var OPENAI_COMPATIBLE_BASE_URL_ENV = [
|
|
975
|
+
{ prefix: "https://api.openai.com/", envName: "OPENAI_API_KEY" },
|
|
976
|
+
{ prefix: "https://openrouter.ai/api/", envName: "OPENROUTER_API_KEY" },
|
|
977
|
+
{ prefix: "https://api.together.xyz/", envName: "TOGETHER_API_KEY" },
|
|
978
|
+
{ prefix: "https://api.groq.com/openai/", envName: "GROQ_API_KEY" }
|
|
979
|
+
];
|
|
980
|
+
var PROVIDER_API_KEY_ENV = {
|
|
981
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
982
|
+
claude: "ANTHROPIC_API_KEY",
|
|
983
|
+
groq: "GROQ_API_KEY",
|
|
984
|
+
openai: "OPENAI_API_KEY",
|
|
985
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
986
|
+
together: "TOGETHER_API_KEY"
|
|
987
|
+
};
|
|
988
|
+
function normalizeBaseUrl(baseUrl) {
|
|
989
|
+
if (!baseUrl) {
|
|
990
|
+
return void 0;
|
|
991
|
+
}
|
|
992
|
+
return `${baseUrl.replace(/\/+$/, "")}/`.toLowerCase();
|
|
993
|
+
}
|
|
994
|
+
function resolveCompatibleEnvName(baseUrl) {
|
|
995
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
996
|
+
if (!normalized) {
|
|
997
|
+
return void 0;
|
|
998
|
+
}
|
|
999
|
+
const match = OPENAI_COMPATIBLE_BASE_URL_ENV.find(
|
|
1000
|
+
(entry) => normalized.startsWith(entry.prefix)
|
|
1001
|
+
);
|
|
1002
|
+
return match?.envName;
|
|
1003
|
+
}
|
|
1004
|
+
function resolveProviderApiKey(provider, baseUrl, env) {
|
|
1005
|
+
if (env.SIFT_PROVIDER_API_KEY) {
|
|
1006
|
+
return env.SIFT_PROVIDER_API_KEY;
|
|
1007
|
+
}
|
|
1008
|
+
if (provider === "openai-compatible") {
|
|
1009
|
+
const envName2 = resolveCompatibleEnvName(baseUrl);
|
|
1010
|
+
return envName2 ? env[envName2] : void 0;
|
|
1011
|
+
}
|
|
1012
|
+
if (!provider) {
|
|
1013
|
+
return void 0;
|
|
1014
|
+
}
|
|
1015
|
+
const envName = PROVIDER_API_KEY_ENV[provider];
|
|
1016
|
+
return envName ? env[envName] : void 0;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
881
1019
|
// src/config/schema.ts
|
|
882
1020
|
import { z } from "zod";
|
|
883
1021
|
var providerNameSchema = z.enum(["openai-compatible"]);
|
|
@@ -888,6 +1026,7 @@ var outputFormatSchema = z.enum([
|
|
|
888
1026
|
"verdict"
|
|
889
1027
|
]);
|
|
890
1028
|
var responseModeSchema = z.enum(["text", "json"]);
|
|
1029
|
+
var jsonResponseFormatModeSchema = z.enum(["auto", "on", "off"]);
|
|
891
1030
|
var promptPolicyNameSchema = z.enum([
|
|
892
1031
|
"test-status",
|
|
893
1032
|
"audit-critical",
|
|
@@ -901,6 +1040,7 @@ var providerConfigSchema = z.object({
|
|
|
901
1040
|
model: z.string().min(1),
|
|
902
1041
|
baseUrl: z.string().url(),
|
|
903
1042
|
apiKey: z.string().optional(),
|
|
1043
|
+
jsonResponseFormat: jsonResponseFormatModeSchema,
|
|
904
1044
|
timeoutMs: z.number().int().positive(),
|
|
905
1045
|
temperature: z.number().min(0).max(2),
|
|
906
1046
|
maxOutputTokens: z.number().int().positive()
|
|
@@ -954,14 +1094,25 @@ function mergeDefined(base, override) {
|
|
|
954
1094
|
}
|
|
955
1095
|
return result;
|
|
956
1096
|
}
|
|
957
|
-
function
|
|
1097
|
+
function stripApiKey(overrides) {
|
|
1098
|
+
if (!overrides?.provider || overrides.provider.apiKey === void 0) {
|
|
1099
|
+
return overrides;
|
|
1100
|
+
}
|
|
1101
|
+
return {
|
|
1102
|
+
...overrides,
|
|
1103
|
+
provider: {
|
|
1104
|
+
...overrides.provider,
|
|
1105
|
+
apiKey: void 0
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
}
|
|
1109
|
+
function buildNonCredentialEnvOverrides(env) {
|
|
958
1110
|
const overrides = {};
|
|
959
|
-
if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.
|
|
1111
|
+
if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.SIFT_TIMEOUT_MS) {
|
|
960
1112
|
overrides.provider = {
|
|
961
1113
|
provider: env.SIFT_PROVIDER,
|
|
962
1114
|
model: env.SIFT_MODEL,
|
|
963
1115
|
baseUrl: env.SIFT_BASE_URL,
|
|
964
|
-
apiKey: env.SIFT_API_KEY,
|
|
965
1116
|
timeoutMs: env.SIFT_TIMEOUT_MS ? Number(env.SIFT_TIMEOUT_MS) : void 0
|
|
966
1117
|
};
|
|
967
1118
|
}
|
|
@@ -973,12 +1124,40 @@ function buildEnvOverrides(env) {
|
|
|
973
1124
|
}
|
|
974
1125
|
return overrides;
|
|
975
1126
|
}
|
|
1127
|
+
function buildCredentialEnvOverrides(env, context) {
|
|
1128
|
+
const apiKey = resolveProviderApiKey(context.provider, context.baseUrl, env);
|
|
1129
|
+
if (apiKey === void 0) {
|
|
1130
|
+
return {};
|
|
1131
|
+
}
|
|
1132
|
+
return {
|
|
1133
|
+
provider: {
|
|
1134
|
+
apiKey
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
}
|
|
976
1138
|
function resolveConfig(options = {}) {
|
|
977
1139
|
const env = options.env ?? process.env;
|
|
978
1140
|
const fileConfig = loadRawConfig(options.configPath);
|
|
979
|
-
const
|
|
1141
|
+
const nonCredentialEnvConfig = buildNonCredentialEnvOverrides(env);
|
|
1142
|
+
const contextConfig = mergeDefined(
|
|
1143
|
+
mergeDefined(
|
|
1144
|
+
mergeDefined(defaultConfig, fileConfig),
|
|
1145
|
+
nonCredentialEnvConfig
|
|
1146
|
+
),
|
|
1147
|
+
stripApiKey(options.cliOverrides) ?? {}
|
|
1148
|
+
);
|
|
1149
|
+
const credentialEnvConfig = buildCredentialEnvOverrides(env, {
|
|
1150
|
+
provider: contextConfig.provider.provider,
|
|
1151
|
+
baseUrl: contextConfig.provider.baseUrl
|
|
1152
|
+
});
|
|
980
1153
|
const merged = mergeDefined(
|
|
981
|
-
mergeDefined(
|
|
1154
|
+
mergeDefined(
|
|
1155
|
+
mergeDefined(
|
|
1156
|
+
mergeDefined(defaultConfig, fileConfig),
|
|
1157
|
+
nonCredentialEnvConfig
|
|
1158
|
+
),
|
|
1159
|
+
credentialEnvConfig
|
|
1160
|
+
),
|
|
982
1161
|
options.cliOverrides ?? {}
|
|
983
1162
|
);
|
|
984
1163
|
return siftConfigSchema.parse(merged);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bilalimamoglu/sift",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Agent-first command-output reduction layer for agents, CI, and automation.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,6 +37,14 @@
|
|
|
37
37
|
],
|
|
38
38
|
"author": "Bilal Imamoglu",
|
|
39
39
|
"license": "MIT",
|
|
40
|
+
"repository": {
|
|
41
|
+
"type": "git",
|
|
42
|
+
"url": "git+https://github.com/bilalimamoglu/sift.git"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/bilalimamoglu/sift#readme",
|
|
45
|
+
"bugs": {
|
|
46
|
+
"url": "https://github.com/bilalimamoglu/sift/issues"
|
|
47
|
+
},
|
|
40
48
|
"dependencies": {
|
|
41
49
|
"cac": "^6.7.14",
|
|
42
50
|
"picocolors": "^1.1.1",
|