@combycode/llm-sdk 1.0.0 → 1.1.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 +38 -0
- package/README.md +1 -1
- package/dist/index.browser.js +371 -63
- package/dist/index.d.ts +4 -3
- package/dist/index.js +371 -63
- package/dist/llm/client.d.ts +9 -0
- package/dist/llm/moderation/native.d.ts +18 -0
- package/dist/llm/moderation/runner.d.ts +33 -0
- package/dist/llm/moderation/types.d.ts +75 -0
- package/dist/llm/providers/google/tiers.d.ts +15 -0
- package/dist/llm/types/options.d.ts +5 -0
- package/dist/llm/types/request.d.ts +10 -0
- package/dist/llm/types/response.d.ts +23 -0
- package/dist/llm/types/stream.d.ts +10 -0
- package/dist/llm/types/tools.d.ts +21 -0
- package/dist/plugins/model-catalog/catalog.d.ts +3 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,43 @@ All notable changes to `@combycode/llm-sdk` are documented here. The format foll
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/) and the project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [1.1.0] - 2026-06-30
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Hosted MCP tool `tunnel_id` target (OpenAI Secure MCP Tunnel) — reach a private/local MCP
|
|
13
|
+
server with no public URL alongside the existing `server_url` / `connector_id` targets. The
|
|
14
|
+
`mcp` builtin already forwards `params`; added the exported `McpToolParams` type for editor
|
|
15
|
+
help and a regression test locking the forwarding. (Realtime MCP tooling tracked separately.)
|
|
16
|
+
- `ThinkingConfig.context` (`'auto' | 'current_turn' | 'all_turns'`) — maps to OpenAI's
|
|
17
|
+
Responses `reasoning.context`, controlling which prior-turn reasoning items are rendered back
|
|
18
|
+
to the model across a stateful conversation. OpenAI Responses-only; ignored by other providers.
|
|
19
|
+
- Inline moderation via the `moderation` request option on `complete()`/`stream()`
|
|
20
|
+
(parity with OpenAI's `moderation` request field, extended to all providers). Report-only:
|
|
21
|
+
results attach to `CompletionResponse.moderation` (`ModerationReport`) and never block the call.
|
|
22
|
+
Native on the OpenAI provider (one round-trip on both Responses and Chat Completions); emulated
|
|
23
|
+
via OpenAI's moderations endpoint on every other provider (`mode: 'native' | 'emulate'`).
|
|
24
|
+
Streaming supports three strategies (`buffer` default / `parallel` / `post`) trading latency for
|
|
25
|
+
how early the flag reaches the consumer, surfaced as a `moderation` stream event. Emulation
|
|
26
|
+
requires an OpenAI key (reused from the client when it is the OpenAI provider, else
|
|
27
|
+
`moderation.apiKey`); missing key throws.
|
|
28
|
+
- Unified `CompletionResponse.files` (`FileOutput[]`) - files produced by hosted tools
|
|
29
|
+
(code execution, etc.), independent of `media`. The Anthropic adapter surfaces
|
|
30
|
+
code-execution file outputs there by file id; OpenAI/Google/xAI producers to follow.
|
|
31
|
+
- Model catalog: new `ModelInfo.availability` field (`limited` / `preview`, vs default
|
|
32
|
+
generally-available) so gated / early-access models are distinguishable from the
|
|
33
|
+
`status` lifecycle. (Entries for specific limited/preview models are populated by the
|
|
34
|
+
catalog pipeline.)
|
|
35
|
+
- Anthropic: the unified `code_interpreter` builtin now maps to Anthropic's hosted
|
|
36
|
+
`code_execution` tool (GA on Messages) - it was previously silently skipped. Hosted
|
|
37
|
+
code execution is now usable across Anthropic / OpenAI / Google through one interface.
|
|
38
|
+
- Google service tier, both directions (parity with OpenAI/Anthropic): a requested
|
|
39
|
+
unified `serviceTier` (`flex`/`standard`/`priority`) maps to Google's top-level
|
|
40
|
+
request field, and the billed `usageMetadata.serviceTier` is read back into
|
|
41
|
+
`usage.serviceTier` / `usage.pricingTier` for tiered cost tracking.
|
|
42
|
+
(New `providers/google/tiers.ts`.)
|
|
43
|
+
|
|
7
44
|
## [1.0.0] - 2026-06-13
|
|
8
45
|
|
|
9
46
|
First public release.
|
|
@@ -16,4 +53,5 @@ First public release.
|
|
|
16
53
|
- Service tiers end to end (request → bill → cost).
|
|
17
54
|
- Cross-environment: runs on Node, Bun, and the browser. ESM, zero runtime deps.
|
|
18
55
|
|
|
56
|
+
[1.1.0]: https://github.com/combycode/llm-sdk-ts/releases/tag/v1.1.0
|
|
19
57
|
[1.0.0]: https://github.com/combycode/llm-sdk-ts/releases/tag/v1.0.0
|
package/README.md
CHANGED
|
@@ -28,7 +28,7 @@ Requires Node ≥ 18 or Bun ≥ 1.1.
|
|
|
28
28
|
import { complete } from '@combycode/llm-sdk';
|
|
29
29
|
|
|
30
30
|
const r = await complete({
|
|
31
|
-
model: 'anthropic/claude-haiku-4
|
|
31
|
+
model: 'anthropic/claude-haiku-4.5', // provider/model, sent verbatim
|
|
32
32
|
apiKey: process.env.ANTHROPIC_API_KEY,
|
|
33
33
|
prompt: 'Say hello in one word.',
|
|
34
34
|
});
|
package/dist/index.browser.js
CHANGED
|
@@ -22177,6 +22177,7 @@ var ModelCatalog = class {
|
|
|
22177
22177
|
family: info.family,
|
|
22178
22178
|
version: info.version,
|
|
22179
22179
|
status: info.status,
|
|
22180
|
+
availability: info.availability,
|
|
22180
22181
|
active: info.active,
|
|
22181
22182
|
deprecation: info.deprecation
|
|
22182
22183
|
});
|
|
@@ -22264,6 +22265,168 @@ var ModelCatalog = class {
|
|
|
22264
22265
|
}
|
|
22265
22266
|
};
|
|
22266
22267
|
|
|
22268
|
+
// src/llm/providers/openai/moderations.ts
|
|
22269
|
+
var OPENAI_MODERATION_BASE_URL = "https://api.openai.com";
|
|
22270
|
+
var OPENAI_MODERATION_PATH = "/v1/moderations";
|
|
22271
|
+
var OPENAI_MODERATION_DEFAULT_MODEL = "omni-moderation-latest";
|
|
22272
|
+
var OpenAIModerationAdapter = class {
|
|
22273
|
+
apiKey;
|
|
22274
|
+
baseURL;
|
|
22275
|
+
constructor(config) {
|
|
22276
|
+
this.apiKey = config.apiKey;
|
|
22277
|
+
this.baseURL = config.baseURL ?? OPENAI_MODERATION_BASE_URL;
|
|
22278
|
+
}
|
|
22279
|
+
async moderate(input, model, fetch2) {
|
|
22280
|
+
const res = await fetch2({
|
|
22281
|
+
url: `${this.baseURL}${OPENAI_MODERATION_PATH}`,
|
|
22282
|
+
method: "POST",
|
|
22283
|
+
headers: { authorization: `Bearer ${this.apiKey}`, "content-type": "application/json" },
|
|
22284
|
+
body: { model, input },
|
|
22285
|
+
provider: "openai",
|
|
22286
|
+
model,
|
|
22287
|
+
responseType: "json"
|
|
22288
|
+
});
|
|
22289
|
+
if (res.status >= 400) {
|
|
22290
|
+
throw new Error(`OpenAI moderations failed (${res.status}): ${JSON.stringify(res.body)}`);
|
|
22291
|
+
}
|
|
22292
|
+
const data = res.body;
|
|
22293
|
+
return (data.results ?? []).map(parseRawResult);
|
|
22294
|
+
}
|
|
22295
|
+
};
|
|
22296
|
+
function parseRawResult(r) {
|
|
22297
|
+
return {
|
|
22298
|
+
flagged: r.flagged,
|
|
22299
|
+
categories: r.categories,
|
|
22300
|
+
categoryScores: r.category_scores,
|
|
22301
|
+
categoryAppliedInputTypes: r.category_applied_input_types
|
|
22302
|
+
};
|
|
22303
|
+
}
|
|
22304
|
+
|
|
22305
|
+
// src/llm/moderation/types.ts
|
|
22306
|
+
var MODERATION_DEFAULT_MODEL = "omni-moderation-latest";
|
|
22307
|
+
var MODERATION_DEFAULT_STRATEGY = "buffer";
|
|
22308
|
+
var MODERATION_DEFAULT_INTERVAL = 400;
|
|
22309
|
+
|
|
22310
|
+
// src/llm/moderation/runner.ts
|
|
22311
|
+
function resolveModerationMode(provider, mod) {
|
|
22312
|
+
if (mod.mode) return mod.mode;
|
|
22313
|
+
return provider === "openai" ? "native" : "emulate";
|
|
22314
|
+
}
|
|
22315
|
+
function moderationInputText(messages) {
|
|
22316
|
+
const lastUser = [...messages].reverse().find((m) => m.role === "user");
|
|
22317
|
+
if (!lastUser) return "";
|
|
22318
|
+
return typeof lastUser.content === "string" ? lastUser.content : contentText(lastUser.content);
|
|
22319
|
+
}
|
|
22320
|
+
function emptyResult() {
|
|
22321
|
+
return {
|
|
22322
|
+
flagged: false,
|
|
22323
|
+
categories: {},
|
|
22324
|
+
categoryScores: {}
|
|
22325
|
+
};
|
|
22326
|
+
}
|
|
22327
|
+
async function runModeration(text, cfg) {
|
|
22328
|
+
if (!text) return emptyResult();
|
|
22329
|
+
try {
|
|
22330
|
+
const adapter = new OpenAIModerationAdapter({ apiKey: cfg.apiKey });
|
|
22331
|
+
const results = await adapter.moderate(text, cfg.model, cfg.fetch);
|
|
22332
|
+
return results[0] ?? emptyResult();
|
|
22333
|
+
} catch (e) {
|
|
22334
|
+
return { error: e instanceof Error ? e.message : String(e) };
|
|
22335
|
+
}
|
|
22336
|
+
}
|
|
22337
|
+
function moderationModel(mod) {
|
|
22338
|
+
return mod.model ?? MODERATION_DEFAULT_MODEL;
|
|
22339
|
+
}
|
|
22340
|
+
function emitModerationZeroCost(hooks, model) {
|
|
22341
|
+
const entry = {
|
|
22342
|
+
id: crypto.randomUUID(),
|
|
22343
|
+
timestamp: Date.now(),
|
|
22344
|
+
provider: "openai",
|
|
22345
|
+
model,
|
|
22346
|
+
tokens: { input: 0, output: 0, cached: 0, cacheWrite: 0, reasoning: 0 },
|
|
22347
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, reasoning: 0, total: 0, source: "calculated" },
|
|
22348
|
+
providerEvidence: { note: "free: moderations endpoint not billed" },
|
|
22349
|
+
tags: { provider: "openai", model, type: "moderation" }
|
|
22350
|
+
};
|
|
22351
|
+
hooks.emitSync("onCostEntry", { entry, runningTotal: 0 });
|
|
22352
|
+
}
|
|
22353
|
+
function outputEvent(result) {
|
|
22354
|
+
return { type: "moderation", phase: "output", result, source: "emulated" };
|
|
22355
|
+
}
|
|
22356
|
+
function wrapModeratedStream(raw, strategy, interval, moderate2) {
|
|
22357
|
+
const step = interval > 0 ? interval : MODERATION_DEFAULT_INTERVAL;
|
|
22358
|
+
if (strategy === "post") return wrapPost(raw, moderate2);
|
|
22359
|
+
if (strategy === "parallel") return wrapParallel(raw, step, moderate2);
|
|
22360
|
+
return wrapBuffer(raw, step, moderate2);
|
|
22361
|
+
}
|
|
22362
|
+
async function* wrapPost(raw, moderate2) {
|
|
22363
|
+
let acc = "";
|
|
22364
|
+
for await (const ev of raw) {
|
|
22365
|
+
if (ev.type === "text") acc += ev.text;
|
|
22366
|
+
yield ev;
|
|
22367
|
+
}
|
|
22368
|
+
if (acc) yield outputEvent(await moderate2(acc));
|
|
22369
|
+
}
|
|
22370
|
+
async function* wrapBuffer(raw, interval, moderate2) {
|
|
22371
|
+
let acc = "";
|
|
22372
|
+
let checkedAt = 0;
|
|
22373
|
+
let hold = [];
|
|
22374
|
+
for await (const ev of raw) {
|
|
22375
|
+
hold.push(ev);
|
|
22376
|
+
if (ev.type === "text") {
|
|
22377
|
+
acc += ev.text;
|
|
22378
|
+
if (acc.length - checkedAt >= interval || ev.text.includes("\n")) {
|
|
22379
|
+
checkedAt = acc.length;
|
|
22380
|
+
yield outputEvent(await moderate2(acc));
|
|
22381
|
+
for (const h of hold) yield h;
|
|
22382
|
+
hold = [];
|
|
22383
|
+
}
|
|
22384
|
+
}
|
|
22385
|
+
}
|
|
22386
|
+
if (acc.length > checkedAt) yield outputEvent(await moderate2(acc));
|
|
22387
|
+
for (const h of hold) yield h;
|
|
22388
|
+
}
|
|
22389
|
+
async function* wrapParallel(raw, interval, moderate2) {
|
|
22390
|
+
const iter = raw[Symbol.asyncIterator]();
|
|
22391
|
+
const pending = /* @__PURE__ */ new Set();
|
|
22392
|
+
let acc = "";
|
|
22393
|
+
let checkedAt = 0;
|
|
22394
|
+
let rawDone = false;
|
|
22395
|
+
let rawNext = iter.next();
|
|
22396
|
+
const schedule = (text) => {
|
|
22397
|
+
const p = moderate2(text).then((result) => {
|
|
22398
|
+
pending.delete(p);
|
|
22399
|
+
return { tag: "mod", event: outputEvent(result) };
|
|
22400
|
+
});
|
|
22401
|
+
pending.add(p);
|
|
22402
|
+
};
|
|
22403
|
+
while (!rawDone || pending.size > 0) {
|
|
22404
|
+
const racers = [];
|
|
22405
|
+
if (!rawDone) racers.push(rawNext.then((result) => ({ tag: "raw", result })));
|
|
22406
|
+
for (const p of pending) racers.push(p);
|
|
22407
|
+
const winner = await Promise.race(racers);
|
|
22408
|
+
if (winner.tag === "mod") {
|
|
22409
|
+
yield winner.event;
|
|
22410
|
+
continue;
|
|
22411
|
+
}
|
|
22412
|
+
if (winner.result.done) {
|
|
22413
|
+
rawDone = true;
|
|
22414
|
+
if (acc.length > checkedAt) schedule(acc);
|
|
22415
|
+
continue;
|
|
22416
|
+
}
|
|
22417
|
+
const ev = winner.result.value;
|
|
22418
|
+
yield ev;
|
|
22419
|
+
if (ev.type === "text") {
|
|
22420
|
+
acc += ev.text;
|
|
22421
|
+
if (acc.length - checkedAt >= interval) {
|
|
22422
|
+
checkedAt = acc.length;
|
|
22423
|
+
schedule(acc);
|
|
22424
|
+
}
|
|
22425
|
+
}
|
|
22426
|
+
rawNext = iter.next();
|
|
22427
|
+
}
|
|
22428
|
+
}
|
|
22429
|
+
|
|
22267
22430
|
// src/util/duration.ts
|
|
22268
22431
|
var UNIT_MS = {
|
|
22269
22432
|
s: 1e3,
|
|
@@ -22400,6 +22563,7 @@ var LLMClient = class {
|
|
|
22400
22563
|
mode;
|
|
22401
22564
|
batchable;
|
|
22402
22565
|
adapter;
|
|
22566
|
+
apiKey;
|
|
22403
22567
|
fetchFn;
|
|
22404
22568
|
fetchStreamFn;
|
|
22405
22569
|
priority;
|
|
@@ -22423,6 +22587,7 @@ var LLMClient = class {
|
|
|
22423
22587
|
this.provider = config.provider;
|
|
22424
22588
|
this.model = config.model;
|
|
22425
22589
|
this.system = config.system;
|
|
22590
|
+
this.apiKey = config.apiKey;
|
|
22426
22591
|
this.hooks = config.hooks ?? new HookBus();
|
|
22427
22592
|
this.fetchFn = config.fetch;
|
|
22428
22593
|
this.fetchStreamFn = config.fetchStream ?? null;
|
|
@@ -22476,6 +22641,34 @@ var LLMClient = class {
|
|
|
22476
22641
|
}
|
|
22477
22642
|
};
|
|
22478
22643
|
}
|
|
22644
|
+
// ─── Moderation helpers ───────────────────────────────────────────────
|
|
22645
|
+
/** Resolve the OpenAI key the emulated moderation path needs. Reuses the
|
|
22646
|
+
* client's own key when the provider is OpenAI; otherwise requires an explicit
|
|
22647
|
+
* one. Throws when none is resolvable (report-only feature, but the call still
|
|
22648
|
+
* needs a key to reach the moderations endpoint). */
|
|
22649
|
+
resolveModerationKey(mod) {
|
|
22650
|
+
const key = mod.apiKey ?? (this.provider === "openai" ? this.apiKey : void 0);
|
|
22651
|
+
if (!key) {
|
|
22652
|
+
throw new Error(
|
|
22653
|
+
"moderation: emulated moderation requires an OpenAI API key (the only public moderations endpoint). Pass moderation.apiKey, or use the OpenAI provider."
|
|
22654
|
+
);
|
|
22655
|
+
}
|
|
22656
|
+
return key;
|
|
22657
|
+
}
|
|
22658
|
+
moderationConfig(mod) {
|
|
22659
|
+
return {
|
|
22660
|
+
apiKey: this.resolveModerationKey(mod),
|
|
22661
|
+
model: moderationModel(mod),
|
|
22662
|
+
fetch: this.fetchFn
|
|
22663
|
+
};
|
|
22664
|
+
}
|
|
22665
|
+
/** Fold a moderation stream event into the accumulating report. */
|
|
22666
|
+
mergeModeration(prev, phase, result, source) {
|
|
22667
|
+
const next = prev && prev.source === source ? { ...prev } : { source };
|
|
22668
|
+
if (phase === "input") next.input = result;
|
|
22669
|
+
else next.output = result;
|
|
22670
|
+
return next;
|
|
22671
|
+
}
|
|
22479
22672
|
/** Submit a request. Returns the parsed CompletionResponse. */
|
|
22480
22673
|
async complete(input, options = {}) {
|
|
22481
22674
|
const rawMessages = normalizeInput(input);
|
|
@@ -22496,6 +22689,7 @@ var LLMClient = class {
|
|
|
22496
22689
|
thinking: options.thinking,
|
|
22497
22690
|
cache: options.cache,
|
|
22498
22691
|
serviceTier: options.serviceTier,
|
|
22692
|
+
moderation: options.moderation,
|
|
22499
22693
|
providerOptions: options.providerOptions,
|
|
22500
22694
|
audio: options.audio,
|
|
22501
22695
|
outputModalities: options.outputModalities,
|
|
@@ -22551,6 +22745,8 @@ var LLMClient = class {
|
|
|
22551
22745
|
await this.hooks.emit("onBeforeSubmit", submitCtx);
|
|
22552
22746
|
const inputChars = JSON.stringify(normalized.messages).length;
|
|
22553
22747
|
const estimatedInputTokens = Math.ceil(inputChars / 4);
|
|
22748
|
+
const mod = options.moderation;
|
|
22749
|
+
const moderationCfg = mod && resolveModerationMode(this.provider, mod) === "emulate" ? this.moderationConfig(mod) : void 0;
|
|
22554
22750
|
const start = performance.now();
|
|
22555
22751
|
let response;
|
|
22556
22752
|
if (submitCtx.intercepted && submitCtx.resultPromise) {
|
|
@@ -22576,6 +22772,22 @@ var LLMClient = class {
|
|
|
22576
22772
|
}
|
|
22577
22773
|
const latencyMs = performance.now() - start;
|
|
22578
22774
|
const result = this.adapter.parseResponse(response.body, latencyMs);
|
|
22775
|
+
if (mod && moderationCfg) {
|
|
22776
|
+
const cfg = moderationCfg;
|
|
22777
|
+
const doInput = mod.input ?? true;
|
|
22778
|
+
const doOutput = mod.output ?? true;
|
|
22779
|
+
const [inputEntry, outputEntry] = await Promise.all([
|
|
22780
|
+
doInput ? runModeration(moderationInputText(normalized.messages), cfg) : Promise.resolve(void 0),
|
|
22781
|
+
doOutput ? runModeration(result.text, cfg) : Promise.resolve(void 0)
|
|
22782
|
+
]);
|
|
22783
|
+
if (doInput) emitModerationZeroCost(this.hooks, cfg.model);
|
|
22784
|
+
if (doOutput) emitModerationZeroCost(this.hooks, cfg.model);
|
|
22785
|
+
result.moderation = {
|
|
22786
|
+
source: "emulated",
|
|
22787
|
+
...inputEntry ? { input: inputEntry } : {},
|
|
22788
|
+
...outputEntry ? { output: outputEntry } : {}
|
|
22789
|
+
};
|
|
22790
|
+
}
|
|
22579
22791
|
await this.hooks.emit("onCompletion", {
|
|
22580
22792
|
provider: this.provider,
|
|
22581
22793
|
model: this.model,
|
|
@@ -22623,6 +22835,7 @@ var LLMClient = class {
|
|
|
22623
22835
|
thinking: options.thinking,
|
|
22624
22836
|
cache: options.cache,
|
|
22625
22837
|
serviceTier: options.serviceTier,
|
|
22838
|
+
moderation: options.moderation,
|
|
22626
22839
|
providerOptions: options.providerOptions,
|
|
22627
22840
|
audio: options.audio,
|
|
22628
22841
|
outputModalities: options.outputModalities,
|
|
@@ -22666,29 +22879,65 @@ var LLMClient = class {
|
|
|
22666
22879
|
let thinking = "";
|
|
22667
22880
|
let usage = emptyUsage();
|
|
22668
22881
|
let finishReason = "stop";
|
|
22669
|
-
|
|
22670
|
-
|
|
22671
|
-
|
|
22672
|
-
|
|
22673
|
-
|
|
22674
|
-
|
|
22675
|
-
for (const
|
|
22676
|
-
|
|
22677
|
-
|
|
22678
|
-
|
|
22679
|
-
|
|
22680
|
-
|
|
22681
|
-
|
|
22682
|
-
|
|
22683
|
-
|
|
22684
|
-
|
|
22685
|
-
|
|
22686
|
-
|
|
22687
|
-
|
|
22688
|
-
|
|
22689
|
-
|
|
22690
|
-
|
|
22882
|
+
let moderationReport;
|
|
22883
|
+
const fetchStream = this.fetchStreamFn;
|
|
22884
|
+
const adapter = this.adapter;
|
|
22885
|
+
const queueName = this.queueName;
|
|
22886
|
+
const priority = this.priority;
|
|
22887
|
+
async function* rawEvents() {
|
|
22888
|
+
for await (const sseEvent of fetchStream(httpReq, {
|
|
22889
|
+
queueName,
|
|
22890
|
+
priority,
|
|
22891
|
+
ctx
|
|
22892
|
+
})) {
|
|
22893
|
+
for (const ev of adapter.parseStreamEvent(sseEvent)) yield ev;
|
|
22894
|
+
}
|
|
22895
|
+
}
|
|
22896
|
+
const mod = options.moderation;
|
|
22897
|
+
const emulate = !!mod && resolveModerationMode(this.provider, mod) === "emulate";
|
|
22898
|
+
let eventStream = rawEvents();
|
|
22899
|
+
if (mod && emulate && (mod.output ?? true)) {
|
|
22900
|
+
const cfg = this.moderationConfig(mod);
|
|
22901
|
+
const hooks = this.hooks;
|
|
22902
|
+
const strategy = mod.stream?.strategy ?? MODERATION_DEFAULT_STRATEGY;
|
|
22903
|
+
const interval = mod.stream?.interval ?? MODERATION_DEFAULT_INTERVAL;
|
|
22904
|
+
eventStream = wrapModeratedStream(eventStream, strategy, interval, async (t) => {
|
|
22905
|
+
const r = await runModeration(t, cfg);
|
|
22906
|
+
emitModerationZeroCost(hooks, cfg.model);
|
|
22907
|
+
return r;
|
|
22908
|
+
});
|
|
22909
|
+
}
|
|
22910
|
+
if (mod && emulate && (mod.input ?? true)) {
|
|
22911
|
+
const cfg = this.moderationConfig(mod);
|
|
22912
|
+
const inputEntry = await runModeration(moderationInputText(normalized.messages), cfg);
|
|
22913
|
+
emitModerationZeroCost(this.hooks, cfg.model);
|
|
22914
|
+
moderationReport = this.mergeModeration(moderationReport, "input", inputEntry, "emulated");
|
|
22915
|
+
yield { type: "moderation", phase: "input", result: inputEntry, source: "emulated" };
|
|
22916
|
+
}
|
|
22917
|
+
for await (const event of eventStream) {
|
|
22918
|
+
switch (event.type) {
|
|
22919
|
+
case "text":
|
|
22920
|
+
text += event.text;
|
|
22921
|
+
break;
|
|
22922
|
+
case "thinking":
|
|
22923
|
+
thinking += event.text;
|
|
22924
|
+
break;
|
|
22925
|
+
case "usage":
|
|
22926
|
+
usage = event.usage;
|
|
22927
|
+
break;
|
|
22928
|
+
case "done":
|
|
22929
|
+
finishReason = event.finishReason;
|
|
22930
|
+
break;
|
|
22931
|
+
case "moderation":
|
|
22932
|
+
moderationReport = this.mergeModeration(
|
|
22933
|
+
moderationReport,
|
|
22934
|
+
event.phase,
|
|
22935
|
+
event.result,
|
|
22936
|
+
event.source
|
|
22937
|
+
);
|
|
22938
|
+
break;
|
|
22691
22939
|
}
|
|
22940
|
+
yield event;
|
|
22692
22941
|
}
|
|
22693
22942
|
const inputChars = JSON.stringify(normalized.messages).length;
|
|
22694
22943
|
const response = {
|
|
@@ -22701,6 +22950,7 @@ var LLMClient = class {
|
|
|
22701
22950
|
toolCalls: [],
|
|
22702
22951
|
thinking: thinking || null,
|
|
22703
22952
|
media: [],
|
|
22953
|
+
...moderationReport ? { moderation: moderationReport } : {},
|
|
22704
22954
|
latencyMs: performance.now() - start,
|
|
22705
22955
|
raw: null
|
|
22706
22956
|
};
|
|
@@ -23009,6 +23259,9 @@ var AnthropicAdapter = class {
|
|
|
23009
23259
|
if (t.type === "web_search") {
|
|
23010
23260
|
return { type: "web_search_20250305", name: "web_search", max_uses: 5 };
|
|
23011
23261
|
}
|
|
23262
|
+
if (t.type === "code_interpreter") {
|
|
23263
|
+
return { type: "code_execution_20260521", name: "code_execution" };
|
|
23264
|
+
}
|
|
23012
23265
|
return null;
|
|
23013
23266
|
}
|
|
23014
23267
|
const tool = {
|
|
@@ -23119,6 +23372,7 @@ var AnthropicAdapter = class {
|
|
|
23119
23372
|
const content = [];
|
|
23120
23373
|
let thinking = null;
|
|
23121
23374
|
const toolCalls = [];
|
|
23375
|
+
const files = [];
|
|
23122
23376
|
for (const block of contentBlocks) {
|
|
23123
23377
|
if (block.type === "text") {
|
|
23124
23378
|
content.push({ type: "text", text: block.text });
|
|
@@ -23133,6 +23387,15 @@ var AnthropicAdapter = class {
|
|
|
23133
23387
|
};
|
|
23134
23388
|
content.push(tc);
|
|
23135
23389
|
toolCalls.push(tc);
|
|
23390
|
+
} else if (block.type === "code_execution_tool_result") {
|
|
23391
|
+
const result = block.content;
|
|
23392
|
+
if (result?.type === "code_execution_result" && Array.isArray(result.content)) {
|
|
23393
|
+
for (const out of result.content) {
|
|
23394
|
+
if (out.type === "code_execution_output" && typeof out.file_id === "string") {
|
|
23395
|
+
files.push({ id: out.file_id, source: "code_execution" });
|
|
23396
|
+
}
|
|
23397
|
+
}
|
|
23398
|
+
}
|
|
23136
23399
|
}
|
|
23137
23400
|
}
|
|
23138
23401
|
const finishReason = extractFinishReason(toolCalls.length > 0, r.stop_reason, {
|
|
@@ -23147,6 +23410,7 @@ var AnthropicAdapter = class {
|
|
|
23147
23410
|
text: content.filter((p) => p.type === "text").map((p) => p.text).join(""),
|
|
23148
23411
|
toolCalls,
|
|
23149
23412
|
media: [],
|
|
23413
|
+
...files.length ? { files } : {},
|
|
23150
23414
|
thinking,
|
|
23151
23415
|
latencyMs,
|
|
23152
23416
|
raw
|
|
@@ -23452,6 +23716,17 @@ function resolveVoice(provider, voice) {
|
|
|
23452
23716
|
return VOICE_ALIASES[provider]?.[voice] ?? voice;
|
|
23453
23717
|
}
|
|
23454
23718
|
|
|
23719
|
+
// src/llm/providers/google/tiers.ts
|
|
23720
|
+
var REQUEST = /* @__PURE__ */ new Set(["flex", "standard", "priority"]);
|
|
23721
|
+
function googleRequestTier(t) {
|
|
23722
|
+
if (!t) return void 0;
|
|
23723
|
+
return REQUEST.has(t) ? t : void 0;
|
|
23724
|
+
}
|
|
23725
|
+
function googleBilledTier(raw) {
|
|
23726
|
+
if (typeof raw !== "string" || !raw) return {};
|
|
23727
|
+
return { serviceTier: raw, pricingTier: raw.toLowerCase() };
|
|
23728
|
+
}
|
|
23729
|
+
|
|
23455
23730
|
// src/llm/providers/google/constants.ts
|
|
23456
23731
|
var GOOGLE_THINKING_LEVELS = {
|
|
23457
23732
|
low: "LOW",
|
|
@@ -23504,6 +23779,8 @@ var GoogleAdapter = class {
|
|
|
23504
23779
|
contents,
|
|
23505
23780
|
generationConfig: config
|
|
23506
23781
|
};
|
|
23782
|
+
const tier = googleRequestTier(req.serviceTier);
|
|
23783
|
+
if (tier) body.serviceTier = tier;
|
|
23507
23784
|
if (req.system) {
|
|
23508
23785
|
body.systemInstruction = { parts: [{ text: req.system }] };
|
|
23509
23786
|
}
|
|
@@ -23762,7 +24039,9 @@ var GoogleAdapter = class {
|
|
|
23762
24039
|
totalTokens: u.totalTokenCount ?? input + output,
|
|
23763
24040
|
cachedTokens: u.cachedContentTokenCount ?? 0,
|
|
23764
24041
|
cacheWriteTokens: 0,
|
|
23765
|
-
reasoningTokens: u.thoughtsTokenCount ?? 0
|
|
24042
|
+
reasoningTokens: u.thoughtsTokenCount ?? 0,
|
|
24043
|
+
// Billed service tier (output-only `usageMetadata.serviceTier`).
|
|
24044
|
+
...googleBilledTier(u.serviceTier)
|
|
23766
24045
|
};
|
|
23767
24046
|
}
|
|
23768
24047
|
};
|
|
@@ -24637,8 +24916,48 @@ var OpenAIBatchAdapter = class {
|
|
|
24637
24916
|
}
|
|
24638
24917
|
};
|
|
24639
24918
|
|
|
24919
|
+
// src/llm/moderation/native.ts
|
|
24920
|
+
function buildNativeModeration(mod) {
|
|
24921
|
+
return { model: mod.model ?? MODERATION_DEFAULT_MODEL };
|
|
24922
|
+
}
|
|
24923
|
+
function parseNativeModeration(raw) {
|
|
24924
|
+
if (!raw || typeof raw !== "object") return void 0;
|
|
24925
|
+
const m = raw;
|
|
24926
|
+
const input = parseEntry(m.input);
|
|
24927
|
+
const output = parseEntry(m.output);
|
|
24928
|
+
if (!input && !output) return void 0;
|
|
24929
|
+
return {
|
|
24930
|
+
source: "native",
|
|
24931
|
+
...input ? { input } : {},
|
|
24932
|
+
...output ? { output } : {}
|
|
24933
|
+
};
|
|
24934
|
+
}
|
|
24935
|
+
function parseEntry(entry) {
|
|
24936
|
+
if (!entry || typeof entry !== "object") return void 0;
|
|
24937
|
+
const e = entry;
|
|
24938
|
+
if (e.type === "error") {
|
|
24939
|
+
return { error: String(e.message ?? e.code ?? "moderation error") };
|
|
24940
|
+
}
|
|
24941
|
+
if (e.type === "moderation_results" && Array.isArray(e.results)) {
|
|
24942
|
+
const first = e.results[0];
|
|
24943
|
+
return first ? toResult(first) : void 0;
|
|
24944
|
+
}
|
|
24945
|
+
if (e.type === "moderation_result" || e.categories) {
|
|
24946
|
+
return toResult(e);
|
|
24947
|
+
}
|
|
24948
|
+
return void 0;
|
|
24949
|
+
}
|
|
24950
|
+
function toResult(r) {
|
|
24951
|
+
return {
|
|
24952
|
+
flagged: Boolean(r.flagged),
|
|
24953
|
+
categories: r.categories ?? {},
|
|
24954
|
+
categoryScores: r.category_scores ?? {},
|
|
24955
|
+
categoryAppliedInputTypes: r.category_applied_input_types
|
|
24956
|
+
};
|
|
24957
|
+
}
|
|
24958
|
+
|
|
24640
24959
|
// src/llm/providers/openai/tiers.ts
|
|
24641
|
-
var
|
|
24960
|
+
var REQUEST2 = {
|
|
24642
24961
|
auto: "auto",
|
|
24643
24962
|
standard: "default",
|
|
24644
24963
|
priority: "priority",
|
|
@@ -24648,7 +24967,7 @@ var REQUEST = {
|
|
|
24648
24967
|
var KNOWN = /* @__PURE__ */ new Set(["auto", "default", "flex", "scale", "priority"]);
|
|
24649
24968
|
function openaiRequestTier(t) {
|
|
24650
24969
|
if (!t) return void 0;
|
|
24651
|
-
const mapped =
|
|
24970
|
+
const mapped = REQUEST2[t] ?? t;
|
|
24652
24971
|
return KNOWN.has(mapped) ? mapped : "auto";
|
|
24653
24972
|
}
|
|
24654
24973
|
function openaiBilledTier(raw) {
|
|
@@ -24707,6 +25026,9 @@ var OpenAIAdapter = class {
|
|
|
24707
25026
|
if (req.stop) body.stop = req.stop;
|
|
24708
25027
|
const tier = openaiRequestTier(req.serviceTier);
|
|
24709
25028
|
if (tier) body.service_tier = tier;
|
|
25029
|
+
if (req.moderation && req.moderation.mode !== "emulate") {
|
|
25030
|
+
body.moderation = buildNativeModeration(req.moderation);
|
|
25031
|
+
}
|
|
24710
25032
|
const hasAudioInput = req.messages.some(
|
|
24711
25033
|
(m) => Array.isArray(m.content) && m.content.some((p) => p.type === "audio")
|
|
24712
25034
|
);
|
|
@@ -24865,6 +25187,7 @@ var OpenAIAdapter = class {
|
|
|
24865
25187
|
{ tool_calls: "tool_use", length: "length", content_filter: "content_filter" }
|
|
24866
25188
|
);
|
|
24867
25189
|
const reasoningContent = message.reasoning_content ?? null;
|
|
25190
|
+
const moderation = parseNativeModeration(r.moderation);
|
|
24868
25191
|
return {
|
|
24869
25192
|
id: r.id,
|
|
24870
25193
|
model: r.model,
|
|
@@ -24875,12 +25198,22 @@ var OpenAIAdapter = class {
|
|
|
24875
25198
|
toolCalls,
|
|
24876
25199
|
media,
|
|
24877
25200
|
thinking: reasoningContent,
|
|
25201
|
+
...moderation ? { moderation } : {},
|
|
24878
25202
|
latencyMs,
|
|
24879
25203
|
raw
|
|
24880
25204
|
};
|
|
24881
25205
|
}
|
|
24882
25206
|
parseStreamEvent(event) {
|
|
24883
25207
|
const data = JSON.parse(event.data);
|
|
25208
|
+
if (data.moderation) {
|
|
25209
|
+
const report = parseNativeModeration(data.moderation);
|
|
25210
|
+
const out = [];
|
|
25211
|
+
if (report?.input)
|
|
25212
|
+
out.push({ type: "moderation", phase: "input", result: report.input, source: "native" });
|
|
25213
|
+
if (report?.output)
|
|
25214
|
+
out.push({ type: "moderation", phase: "output", result: report.output, source: "native" });
|
|
25215
|
+
if (out.length) return out;
|
|
25216
|
+
}
|
|
24884
25217
|
const choices = data.choices ?? [];
|
|
24885
25218
|
const choice = choices[0];
|
|
24886
25219
|
if (!choice) {
|
|
@@ -25481,6 +25814,9 @@ var OpenAIResponsesAdapter = class {
|
|
|
25481
25814
|
if (req.topP !== void 0) body.top_p = req.topP;
|
|
25482
25815
|
const tier = openaiRequestTier(req.serviceTier);
|
|
25483
25816
|
if (tier) body.service_tier = tier;
|
|
25817
|
+
if (req.moderation && req.moderation.mode !== "emulate") {
|
|
25818
|
+
body.moderation = buildNativeModeration(req.moderation);
|
|
25819
|
+
}
|
|
25484
25820
|
if (req.tools?.length) {
|
|
25485
25821
|
body.tools = req.tools.map((t) => {
|
|
25486
25822
|
if (isFunctionTool(t)) {
|
|
@@ -25519,7 +25855,9 @@ var OpenAIResponsesAdapter = class {
|
|
|
25519
25855
|
if (req.thinking && req.thinking.mode !== "off") {
|
|
25520
25856
|
body.reasoning = {
|
|
25521
25857
|
effort: req.thinking.effort ?? "medium",
|
|
25522
|
-
summary: "auto"
|
|
25858
|
+
summary: "auto",
|
|
25859
|
+
// Cross-turn reasoning persistence (gpt-5/o-series, Responses only).
|
|
25860
|
+
...req.thinking.context ? { context: req.thinking.context } : {}
|
|
25523
25861
|
};
|
|
25524
25862
|
}
|
|
25525
25863
|
return { body };
|
|
@@ -25663,6 +26001,7 @@ var OpenAIResponsesAdapter = class {
|
|
|
25663
26001
|
text = r.output_text;
|
|
25664
26002
|
if (text && content.length === 0) content.push({ type: "text", text });
|
|
25665
26003
|
}
|
|
26004
|
+
const moderation = parseNativeModeration(r.moderation);
|
|
25666
26005
|
return {
|
|
25667
26006
|
id: r.id,
|
|
25668
26007
|
model: r.model ?? "",
|
|
@@ -25673,6 +26012,7 @@ var OpenAIResponsesAdapter = class {
|
|
|
25673
26012
|
toolCalls,
|
|
25674
26013
|
thinking,
|
|
25675
26014
|
media,
|
|
26015
|
+
...moderation ? { moderation } : {},
|
|
25676
26016
|
latencyMs,
|
|
25677
26017
|
raw
|
|
25678
26018
|
};
|
|
@@ -25727,6 +26067,11 @@ var OpenAIResponsesAdapter = class {
|
|
|
25727
26067
|
}
|
|
25728
26068
|
if (type === "response.completed") {
|
|
25729
26069
|
const response = data.response ?? data;
|
|
26070
|
+
const moderation = parseNativeModeration(response.moderation);
|
|
26071
|
+
if (moderation?.input)
|
|
26072
|
+
events.push({ type: "moderation", phase: "input", result: moderation.input, source: "native" });
|
|
26073
|
+
if (moderation?.output)
|
|
26074
|
+
events.push({ type: "moderation", phase: "output", result: moderation.output, source: "native" });
|
|
25730
26075
|
const usage = response.usage;
|
|
25731
26076
|
if (usage) events.push({ type: "usage", usage: this.parseUsage(usage) });
|
|
25732
26077
|
events.push({
|
|
@@ -33606,43 +33951,6 @@ function handoff(name, description, agent, opts = {}) {
|
|
|
33606
33951
|
});
|
|
33607
33952
|
}
|
|
33608
33953
|
|
|
33609
|
-
// src/llm/providers/openai/moderations.ts
|
|
33610
|
-
var OPENAI_MODERATION_BASE_URL = "https://api.openai.com";
|
|
33611
|
-
var OPENAI_MODERATION_PATH = "/v1/moderations";
|
|
33612
|
-
var OPENAI_MODERATION_DEFAULT_MODEL = "omni-moderation-latest";
|
|
33613
|
-
var OpenAIModerationAdapter = class {
|
|
33614
|
-
apiKey;
|
|
33615
|
-
baseURL;
|
|
33616
|
-
constructor(config) {
|
|
33617
|
-
this.apiKey = config.apiKey;
|
|
33618
|
-
this.baseURL = config.baseURL ?? OPENAI_MODERATION_BASE_URL;
|
|
33619
|
-
}
|
|
33620
|
-
async moderate(input, model, fetch2) {
|
|
33621
|
-
const res = await fetch2({
|
|
33622
|
-
url: `${this.baseURL}${OPENAI_MODERATION_PATH}`,
|
|
33623
|
-
method: "POST",
|
|
33624
|
-
headers: { authorization: `Bearer ${this.apiKey}`, "content-type": "application/json" },
|
|
33625
|
-
body: { model, input },
|
|
33626
|
-
provider: "openai",
|
|
33627
|
-
model,
|
|
33628
|
-
responseType: "json"
|
|
33629
|
-
});
|
|
33630
|
-
if (res.status >= 400) {
|
|
33631
|
-
throw new Error(`OpenAI moderations failed (${res.status}): ${JSON.stringify(res.body)}`);
|
|
33632
|
-
}
|
|
33633
|
-
const data = res.body;
|
|
33634
|
-
return (data.results ?? []).map(parseRawResult);
|
|
33635
|
-
}
|
|
33636
|
-
};
|
|
33637
|
-
function parseRawResult(r) {
|
|
33638
|
-
return {
|
|
33639
|
-
flagged: r.flagged,
|
|
33640
|
-
categories: r.categories,
|
|
33641
|
-
categoryScores: r.category_scores,
|
|
33642
|
-
categoryAppliedInputTypes: r.category_applied_input_types
|
|
33643
|
-
};
|
|
33644
|
-
}
|
|
33645
|
-
|
|
33646
33954
|
// src/helpers/moderate.ts
|
|
33647
33955
|
var MODERATION_COST_NOTE = "free: moderations endpoint not billed";
|
|
33648
33956
|
var MODERATION_DEFAULT_PROVIDER = "openai";
|