@combycode/llm-sdk 1.0.0-rc.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 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-5', // provider/model, sent verbatim
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
  });
@@ -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
- for await (const sseEvent of this.fetchStreamFn(httpReq, {
22670
- queueName: this.queueName,
22671
- priority: this.priority,
22672
- ctx
22673
- })) {
22674
- const events = this.adapter.parseStreamEvent(sseEvent);
22675
- for (const event of events) {
22676
- switch (event.type) {
22677
- case "text":
22678
- text += event.text;
22679
- break;
22680
- case "thinking":
22681
- thinking += event.text;
22682
- break;
22683
- case "usage":
22684
- usage = event.usage;
22685
- break;
22686
- case "done":
22687
- finishReason = event.finishReason;
22688
- break;
22689
- }
22690
- yield event;
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 REQUEST = {
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 = REQUEST[t] ?? t;
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";