ship-safe 9.1.2 → 9.2.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.
@@ -27,6 +27,8 @@ class BaseLLMProvider {
27
27
  this.apiKey = apiKey;
28
28
  this.model = options.model || null;
29
29
  this.baseUrl = options.baseUrl || null;
30
+ this.think = options.think || false;
31
+ this.thinkLevel = options.thinkLevel || 'high';
30
32
  }
31
33
 
32
34
  /**
@@ -36,6 +38,16 @@ class BaseLLMProvider {
36
38
  throw new Error(`${this.name}.complete() not implemented`);
37
39
  }
38
40
 
41
+ /**
42
+ * Stream a completion as an async iterable of text chunks.
43
+ * Default implementation falls back to complete() and yields the whole
44
+ * response at once — providers that support real streaming should override.
45
+ */
46
+ async *stream(systemPrompt, userPrompt, options = {}) {
47
+ const text = await this.complete(systemPrompt, userPrompt, options);
48
+ if (text) yield text;
49
+ }
50
+
39
51
  /**
40
52
  * Classify security findings using the LLM.
41
53
  */
@@ -44,7 +56,7 @@ class BaseLLMProvider {
44
56
  const response = await this.complete(
45
57
  'You are a security expert. Respond with JSON only, no markdown.',
46
58
  prompt,
47
- { maxTokens: 4096 }
59
+ { maxTokens: 4096, think: this.think, thinkLevel: this.thinkLevel }
48
60
  );
49
61
  return this.parseJSON(response);
50
62
  }
@@ -172,25 +184,32 @@ class AnthropicProvider extends BaseLLMProvider {
172
184
  class OpenAIProvider extends BaseLLMProvider {
173
185
  constructor(apiKey, options = {}) {
174
186
  super('OpenAI', apiKey, options);
175
- this.model = options.model || 'gpt-4o-mini';
187
+ this.model = options.model || 'gpt-5.5';
176
188
  this.baseUrl = options.baseUrl || 'https://api.openai.com/v1/chat/completions';
177
189
  }
178
190
 
179
191
  async complete(systemPrompt, userPrompt, options = {}) {
192
+ const body = {
193
+ model: this.model,
194
+ max_tokens: options.maxTokens || 2048,
195
+ messages: [
196
+ { role: 'system', content: systemPrompt },
197
+ { role: 'user', content: userPrompt },
198
+ ],
199
+ };
200
+
201
+ if (options.think) {
202
+ body.reasoning_effort = options.thinkLevel || 'high';
203
+ body.max_tokens = options.maxTokens || 16384;
204
+ }
205
+
180
206
  const response = await fetch(this.baseUrl, {
181
207
  method: 'POST',
182
208
  headers: {
183
209
  'Authorization': `Bearer ${this.apiKey}`,
184
210
  'Content-Type': 'application/json',
185
211
  },
186
- body: JSON.stringify({
187
- model: this.model,
188
- max_tokens: options.maxTokens || 2048,
189
- messages: [
190
- { role: 'system', content: systemPrompt },
191
- { role: 'user', content: userPrompt },
192
- ],
193
- }),
212
+ body: JSON.stringify(body),
194
213
  });
195
214
 
196
215
  if (!response.ok) {
@@ -200,6 +219,71 @@ class OpenAIProvider extends BaseLLMProvider {
200
219
  const data = await response.json();
201
220
  return data.choices?.[0]?.message?.content || '';
202
221
  }
222
+
223
+ /**
224
+ * Streaming variant for the OpenAI Chat Completions SSE protocol.
225
+ * Yields content tokens as they arrive. Inherited by every
226
+ * OpenAI-compatible provider (DeepSeek, Kimi, xAI, OpenRouter).
227
+ */
228
+ async *stream(systemPrompt, userPrompt, options = {}) {
229
+ const body = {
230
+ model: this.model,
231
+ max_tokens: options.maxTokens || 2048,
232
+ messages: [
233
+ { role: 'system', content: systemPrompt },
234
+ { role: 'user', content: userPrompt },
235
+ ],
236
+ stream: true,
237
+ };
238
+ if (options.think) {
239
+ body.reasoning_effort = options.thinkLevel || 'high';
240
+ body.max_tokens = options.maxTokens || 16384;
241
+ }
242
+
243
+ const response = await fetch(this.baseUrl, {
244
+ method: 'POST',
245
+ headers: {
246
+ 'Authorization': `Bearer ${this.apiKey}`,
247
+ 'Content-Type': 'application/json',
248
+ 'Accept': 'text/event-stream',
249
+ },
250
+ body: JSON.stringify(body),
251
+ });
252
+
253
+ if (!response.ok) {
254
+ const errBody = await response.text().catch(() => '');
255
+ throw new Error(`${this.name} API error: HTTP ${response.status} ${errBody.slice(0, 200)}`);
256
+ }
257
+
258
+ const reader = response.body.getReader();
259
+ const decoder = new TextDecoder('utf-8');
260
+ let buffer = '';
261
+
262
+ while (true) {
263
+ const { value, done } = await reader.read();
264
+ if (done) break;
265
+ buffer += decoder.decode(value, { stream: true });
266
+
267
+ // SSE events are separated by blank lines; parse line-by-line and only
268
+ // act on `data: ` payloads. Everything else (event:, id:, comments) ignored.
269
+ const lines = buffer.split('\n');
270
+ buffer = lines.pop() ?? ''; // keep trailing partial line for next chunk
271
+
272
+ for (const raw of lines) {
273
+ const line = raw.trim();
274
+ if (!line.startsWith('data:')) continue;
275
+ const payload = line.slice(5).trim();
276
+ if (!payload || payload === '[DONE]') continue;
277
+ try {
278
+ const evt = JSON.parse(payload);
279
+ const delta = evt.choices?.[0]?.delta;
280
+ // Token text (delta.content) — yield as-is. Some providers also send
281
+ // tool_calls; ignored here since the REPL doesn't use tools yet.
282
+ if (delta?.content) yield delta.content;
283
+ } catch { /* malformed chunk — skip */ }
284
+ }
285
+ }
286
+ }
203
287
  }
204
288
 
205
289
  // =============================================================================
@@ -356,6 +440,11 @@ class GemmaProvider extends OllamaProvider {
356
440
 
357
441
  // Well-known OpenAI-compatible base URLs and their default models.
358
442
  const OPENAI_COMPATIBLE_PRESETS = {
443
+ 'gpt-5.5': { baseUrl: 'https://api.openai.com/v1/chat/completions', model: 'gpt-5.5', envKey: 'OPENAI_API_KEY' },
444
+ 'gpt-5.5-pro': { baseUrl: 'https://api.openai.com/v1/chat/completions', model: 'gpt-5.5-pro', envKey: 'OPENAI_API_KEY' },
445
+ 'gpt-5.4': { baseUrl: 'https://api.openai.com/v1/chat/completions', model: 'gpt-5.4', envKey: 'OPENAI_API_KEY' },
446
+ 'gpt-5.4-mini': { baseUrl: 'https://api.openai.com/v1/chat/completions', model: 'gpt-5.4-mini', envKey: 'OPENAI_API_KEY' },
447
+ 'gpt-5.4-nano': { baseUrl: 'https://api.openai.com/v1/chat/completions', model: 'gpt-5.4-nano', envKey: 'OPENAI_API_KEY' },
359
448
  groq: { baseUrl: 'https://api.groq.com/openai/v1/chat/completions', model: 'llama-3.3-70b-versatile', envKey: 'GROQ_API_KEY' },
360
449
  together: { baseUrl: 'https://api.together.xyz/v1/chat/completions', model: 'meta-llama/Llama-3-70b-chat-hf', envKey: 'TOGETHER_API_KEY' },
361
450
  mistral: { baseUrl: 'https://api.mistral.ai/v1/chat/completions', model: 'mistral-large-latest', envKey: 'MISTRAL_API_KEY' },
@@ -381,7 +470,7 @@ class OpenAICompatibleProvider extends OpenAIProvider {
381
470
 
382
471
  /** Models known to support OpenAI function calling reliably */
383
472
  get supportsStructuredOutput() {
384
- return /kimi|moonshot|gpt-4|grok|deepseek|mistral-large/i.test(this.model || '');
473
+ return /kimi|moonshot|gpt-4|gpt-5|grok|deepseek|mistral-large/i.test(this.model || '');
385
474
  }
386
475
 
387
476
  async complete(systemPrompt, userPrompt, options = {}) {
@@ -394,6 +483,10 @@ class OpenAICompatibleProvider extends OpenAIProvider {
394
483
  ],
395
484
  };
396
485
  if (options.jsonMode) body.response_format = { type: 'json_object' };
486
+ if (options.think) {
487
+ body.reasoning_effort = options.thinkLevel || 'high';
488
+ body.max_tokens = options.maxTokens || 16384;
489
+ }
397
490
 
398
491
  const response = await fetch(this.baseUrl, {
399
492
  method: 'POST',
@@ -512,8 +605,10 @@ export function createProvider(provider, apiKey, options = {}) {
512
605
  name.charAt(0).toUpperCase() + name.slice(1),
513
606
  apiKey,
514
607
  {
515
- baseUrl: options.baseUrl || preset.baseUrl,
516
- model: options.model || preset.model || 'default',
608
+ baseUrl: options.baseUrl || preset.baseUrl,
609
+ model: options.model || preset.model || 'default',
610
+ think: options.think,
611
+ thinkLevel: options.thinkLevel,
517
612
  }
518
613
  );
519
614
  }
@@ -526,7 +621,7 @@ export function createProvider(provider, apiKey, options = {}) {
526
621
  throw new Error(
527
622
  `Unknown LLM provider: "${provider}".\n` +
528
623
  `Built-in: anthropic, openai, google, ollama\n` +
529
- `Presets: groq, together, mistral, cohere, deepseek, deepseek-flash, perplexity, lmstudio, xai, kimi\n` +
624
+ `Presets: gpt-5.5, gpt-5.5-pro, gpt-5.4, gpt-5.4-mini, gpt-5.4-nano, groq, together, mistral, cohere, deepseek, deepseek-flash, perplexity, lmstudio, xai, kimi\n` +
530
625
  `Custom: pass any name with --base-url <url>`
531
626
  );
532
627
  }
@@ -542,8 +637,10 @@ export function autoDetectProvider(rootPath, options = {}) {
542
637
  if (options.provider) {
543
638
  const apiKey = resolveApiKey(options.provider, rootPath);
544
639
  return createProvider(options.provider, apiKey, {
545
- model: options.model,
546
- baseUrl: options.baseUrl,
640
+ model: options.model,
641
+ baseUrl: options.baseUrl,
642
+ think: options.think,
643
+ thinkLevel: options.thinkLevel,
547
644
  });
548
645
  }
549
646
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ship-safe",
3
- "version": "9.1.2",
3
+ "version": "9.2.1",
4
4
  "description": "AI-powered multi-agent security platform. 23 agents scan 80+ attack classes including AI integration supply chain (Vercel-class attacks), Hermes Agent deployments (ASI-01–ASI-10), tool registry poisoning, function-call injection, skill permission drift, and agent attestation. Ship Safe × Hermes Agent.",
5
5
  "main": "cli/index.js",
6
6
  "bin": {