llmist 18.2.0 → 18.4.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/dist/index.cjs CHANGED
@@ -40,6 +40,8 @@ var init_execution_tree_aggregator = __esm({
40
40
  this.nodes = nodes;
41
41
  this.getDescendants = getDescendants;
42
42
  }
43
+ nodes;
44
+ getDescendants;
43
45
  // ===========================================================================
44
46
  // Private helpers
45
47
  // ===========================================================================
@@ -2516,21 +2518,39 @@ function createLogger(options = {}) {
2516
2518
  prettyLogTemplate: LOG_TEMPLATE,
2517
2519
  // Use overwrite to redirect tslog's formatted output to file instead of console
2518
2520
  overwrite: useFileLogging ? {
2519
- transportFormatted: (logMetaMarkup, logArgs, _logErrors) => {
2521
+ transportFormatted: (logMetaMarkup, logArgs, logErrors) => {
2520
2522
  const args = logArgs.map(
2521
2523
  (arg) => typeof arg === "string" ? arg : JSON.stringify(arg)
2522
2524
  );
2523
2525
  if (sharedLogFileStream) {
2524
2526
  const meta = stripAnsi(logMetaMarkup);
2525
2527
  const fileArgs = args.map((a) => stripAnsi(a));
2526
- sharedLogFileStream.write(`${meta}${fileArgs.join(" ")}
2528
+ const errTail = logErrors.length ? `
2529
+ ${logErrors.map((e) => stripAnsi(e)).join("\n")}` : "";
2530
+ sharedLogFileStream.write(`${meta}${fileArgs.join(" ")}${errTail}
2527
2531
  `);
2528
2532
  }
2529
2533
  if (teeToConsole) {
2530
- process.stdout.write(`${logMetaMarkup}${args.join(" ")}
2534
+ const errTail = logErrors.length ? `
2535
+ ${logErrors.join("\n")}` : "";
2536
+ process.stdout.write(`${logMetaMarkup}${args.join(" ")}${errTail}
2531
2537
  `);
2532
2538
  }
2533
2539
  }
2540
+ } : defaultType === "pretty" ? {
2541
+ // Diagnostics go to stderr (POSIX convention). tslog's default
2542
+ // pretty transport uses console.log, which pollutes stdout —
2543
+ // breaking consumers whose stdout is content or machine-readable
2544
+ // (piped CLI output, --json/NDJSON modes, --background job refs).
2545
+ transportFormatted: (logMetaMarkup, logArgs, logErrors) => {
2546
+ const args = logArgs.map(
2547
+ (arg) => typeof arg === "string" ? arg : JSON.stringify(arg)
2548
+ );
2549
+ const errTail = logErrors.length ? `
2550
+ ${logErrors.join("\n")}` : "";
2551
+ process.stderr.write(`${logMetaMarkup}${args.join(" ")}${errTail}
2552
+ `);
2553
+ }
2534
2554
  } : void 0
2535
2555
  });
2536
2556
  return logger2;
@@ -3258,6 +3278,18 @@ async function runWithHandlers(agentGenerator, handlers) {
3258
3278
  });
3259
3279
  }
3260
3280
  break;
3281
+ case "gadget_args_partial":
3282
+ if (handlers.onGadgetArgsPartial) {
3283
+ await handlers.onGadgetArgsPartial({
3284
+ gadgetName: event.gadgetName,
3285
+ invocationId: event.invocationId,
3286
+ fieldPath: event.fieldPath,
3287
+ value: event.value,
3288
+ delta: event.delta,
3289
+ isFieldComplete: event.isFieldComplete
3290
+ });
3291
+ }
3292
+ break;
3261
3293
  case "gadget_result":
3262
3294
  if (handlers.onGadgetResult) {
3263
3295
  await handlers.onGadgetResult(event.result);
@@ -5039,7 +5071,7 @@ var init_gadget_output_store = __esm({
5039
5071
  charCount: content.length,
5040
5072
  byteSize: encoder.encode(content).length,
5041
5073
  lineCount: lines.length,
5042
- maxLineLength: lines.reduce((max, line) => Math.max(max, line.length), 0),
5074
+ maxLineLength: lines.reduce((max2, line) => Math.max(max2, line.length), 0),
5043
5075
  timestamp: /* @__PURE__ */ new Date()
5044
5076
  };
5045
5077
  this.outputs.set(id, stored);
@@ -7690,6 +7722,7 @@ var init_base_provider = __esm({
7690
7722
  constructor(client) {
7691
7723
  this.client = client;
7692
7724
  }
7725
+ client;
7693
7726
  /**
7694
7727
  * Template method that defines the skeleton of the streaming algorithm.
7695
7728
  * This orchestrates the four-step process without dictating provider-specific details.
@@ -8059,6 +8092,7 @@ var init_gemini_cache_manager = __esm({
8059
8092
  constructor(client) {
8060
8093
  this.client = client;
8061
8094
  }
8095
+ client;
8062
8096
  activeCache = null;
8063
8097
  /**
8064
8098
  * Get or create a cache for the given content.
@@ -8601,6 +8635,621 @@ var init_gemini_models = __esm({
8601
8635
  }
8602
8636
  });
8603
8637
 
8638
+ // src/research/constants.ts
8639
+ var RESEARCH_DEFAULT_TIMEOUT_MS, GEMINI_RESEARCH_MAX_DURATION_MS, OPENAI_RESEARCH_HTTP_TIMEOUT_MS, OPENROUTER_RESEARCH_HTTP_TIMEOUT_MS, RESEARCH_POLL_INTERVAL_MS, RESEARCH_POLL_MAX_INTERVAL_MS, RESEARCH_POLL_BACKOFF_FACTOR, RESEARCH_STREAM_RECONNECT_MAX_ATTEMPTS, RESEARCH_SHUTDOWN_WARNING_WINDOW_DAYS, RESEARCH_COST_DECIMALS, TOKENS_PER_MILLION, SEARCHES_PER_THOUSAND, MS_PER_DAY;
8640
+ var init_constants3 = __esm({
8641
+ "src/research/constants.ts"() {
8642
+ "use strict";
8643
+ RESEARCH_DEFAULT_TIMEOUT_MS = 36e5;
8644
+ GEMINI_RESEARCH_MAX_DURATION_MS = 36e5;
8645
+ OPENAI_RESEARCH_HTTP_TIMEOUT_MS = 36e5;
8646
+ OPENROUTER_RESEARCH_HTTP_TIMEOUT_MS = 36e5;
8647
+ RESEARCH_POLL_INTERVAL_MS = 1e4;
8648
+ RESEARCH_POLL_MAX_INTERVAL_MS = 6e4;
8649
+ RESEARCH_POLL_BACKOFF_FACTOR = 1.5;
8650
+ RESEARCH_STREAM_RECONNECT_MAX_ATTEMPTS = 5;
8651
+ RESEARCH_SHUTDOWN_WARNING_WINDOW_DAYS = 30;
8652
+ RESEARCH_COST_DECIMALS = 6;
8653
+ TOKENS_PER_MILLION = 1e6;
8654
+ SEARCHES_PER_THOUSAND = 1e3;
8655
+ MS_PER_DAY = 864e5;
8656
+ }
8657
+ });
8658
+
8659
+ // src/research/errors.ts
8660
+ var ResearchNotSupportedError, ResearchJobNotResumableError, ResearchNotPollableError, ResearchTimeoutError, ResearchDeprecatedModelError, ResearchValidationError, ResearchStreamConsumedError;
8661
+ var init_errors2 = __esm({
8662
+ "src/research/errors.ts"() {
8663
+ "use strict";
8664
+ ResearchNotSupportedError = class extends Error {
8665
+ constructor(message) {
8666
+ super(message);
8667
+ this.name = "ResearchNotSupportedError";
8668
+ }
8669
+ };
8670
+ ResearchJobNotResumableError = class extends Error {
8671
+ constructor(message) {
8672
+ super(message);
8673
+ this.name = "ResearchJobNotResumableError";
8674
+ }
8675
+ };
8676
+ ResearchNotPollableError = class extends Error {
8677
+ constructor(message) {
8678
+ super(message);
8679
+ this.name = "ResearchNotPollableError";
8680
+ }
8681
+ };
8682
+ ResearchTimeoutError = class extends Error {
8683
+ timeoutMs;
8684
+ constructor(timeoutMs) {
8685
+ super(
8686
+ `Research run exceeded the client-side time budget of ${timeoutMs}ms. A background job keeps running server-side \u2014 re-attach via its ref.`
8687
+ );
8688
+ this.name = "ResearchTimeoutError";
8689
+ this.timeoutMs = timeoutMs;
8690
+ }
8691
+ };
8692
+ ResearchDeprecatedModelError = class extends Error {
8693
+ modelId;
8694
+ shutdownDate;
8695
+ replacement;
8696
+ constructor(params) {
8697
+ super(
8698
+ `Research model "${params.modelId}" was shut down by its provider on ${params.shutdownDate}.` + (params.replacement ? ` Use "${params.replacement}" instead.` : "")
8699
+ );
8700
+ this.name = "ResearchDeprecatedModelError";
8701
+ this.modelId = params.modelId;
8702
+ this.shutdownDate = params.shutdownDate;
8703
+ this.replacement = params.replacement;
8704
+ }
8705
+ };
8706
+ ResearchValidationError = class extends Error {
8707
+ constructor(message) {
8708
+ super(message);
8709
+ this.name = "ResearchValidationError";
8710
+ }
8711
+ };
8712
+ ResearchStreamConsumedError = class extends Error {
8713
+ constructor() {
8714
+ super(
8715
+ "This research job's event stream was already consumed. Iterate events() (or the job itself) once; use result() for the aggregated outcome."
8716
+ );
8717
+ this.name = "ResearchStreamConsumedError";
8718
+ }
8719
+ };
8720
+ }
8721
+ });
8722
+
8723
+ // src/providers/gemini-research.ts
8724
+ function buildGeminiResearchRequest(options, agentId) {
8725
+ if (options.background === false) {
8726
+ throw new ResearchValidationError(
8727
+ "Gemini deep research requires background execution \u2014 background: false is not supported."
8728
+ );
8729
+ }
8730
+ return {
8731
+ agent: agentId,
8732
+ input: options.query,
8733
+ background: true,
8734
+ store: true,
8735
+ agent_config: {
8736
+ type: "deep-research",
8737
+ thinking_summaries: options.reasoning?.includeThinking === false ? "none" : "auto"
8738
+ },
8739
+ ...options.systemPrompt ? { system_instruction: options.systemPrompt } : {},
8740
+ ...options.previousJobId ? { previous_interaction_id: options.previousJobId } : {},
8741
+ ...options.extra ?? {}
8742
+ };
8743
+ }
8744
+ function mapInteractionStatus(status) {
8745
+ if (status && GEMINI_STATUS_VALUES.has(status)) {
8746
+ return status;
8747
+ }
8748
+ return "in_progress";
8749
+ }
8750
+ function annotationToCitation(annotation) {
8751
+ if (annotation.type !== "url_citation" || !annotation.url) return void 0;
8752
+ return {
8753
+ url: annotation.url,
8754
+ title: annotation.title,
8755
+ startIndex: annotation.start_index,
8756
+ endIndex: annotation.end_index
8757
+ };
8758
+ }
8759
+ function extractReportFromInteraction(interaction) {
8760
+ const parts = [];
8761
+ const citations = [];
8762
+ for (const step of interaction.steps ?? []) {
8763
+ if (step.type !== "model_output") continue;
8764
+ for (const content of step.content ?? []) {
8765
+ if (content.type !== "text") continue;
8766
+ parts.push(content.text);
8767
+ for (const annotation of content.annotations ?? []) {
8768
+ const citation = annotationToCitation(annotation);
8769
+ if (citation) citations.push(citation);
8770
+ }
8771
+ }
8772
+ }
8773
+ return { report: parts.join(""), citations };
8774
+ }
8775
+ function countSearchesInSteps(interaction) {
8776
+ let searches = 0;
8777
+ for (const step of interaction.steps ?? []) {
8778
+ if (step.type === "google_search_call") {
8779
+ searches += step.arguments?.queries?.length ?? 1;
8780
+ }
8781
+ }
8782
+ return searches;
8783
+ }
8784
+ function usageFromInteraction(interaction, streamedSearches) {
8785
+ const usage = interaction.usage;
8786
+ const searches = streamedSearches ?? countSearchesInSteps(interaction);
8787
+ return {
8788
+ inputTokens: usage?.total_input_tokens ?? 0,
8789
+ outputTokens: usage?.total_output_tokens ?? 0,
8790
+ totalTokens: usage?.total_tokens ?? 0,
8791
+ cachedInputTokens: usage?.total_cached_tokens,
8792
+ reasoningTokens: usage?.total_thought_tokens,
8793
+ ...searches > 0 ? { searches } : {}
8794
+ };
8795
+ }
8796
+ function doneEventFromInteraction(interaction, streamedSearches) {
8797
+ const { report, citations } = extractReportFromInteraction(interaction);
8798
+ return {
8799
+ type: "done",
8800
+ result: {
8801
+ status: mapInteractionStatus(interaction.status),
8802
+ report,
8803
+ citations,
8804
+ usage: usageFromInteraction(interaction, streamedSearches),
8805
+ raw: interaction
8806
+ }
8807
+ };
8808
+ }
8809
+ async function* normalizeInteractionsStream(stream2, fetchFinal) {
8810
+ let streamedSearches = 0;
8811
+ const countedSearchSteps = /* @__PURE__ */ new Set();
8812
+ let interactionId;
8813
+ for await (const event of stream2) {
8814
+ const cursor = event.event_id;
8815
+ switch (event.event_type) {
8816
+ case "interaction.created": {
8817
+ const interaction = event.interaction;
8818
+ interactionId = interaction.id;
8819
+ yield { type: "created", jobId: interaction.id, cursor, rawEvent: event };
8820
+ yield { type: "status", status: mapInteractionStatus(interaction.status), cursor };
8821
+ break;
8822
+ }
8823
+ case "interaction.status_update":
8824
+ yield {
8825
+ type: "status",
8826
+ status: mapInteractionStatus(event.status),
8827
+ cursor,
8828
+ rawEvent: event
8829
+ };
8830
+ break;
8831
+ case "step.start": {
8832
+ const step = event.step;
8833
+ if (step.type === "thought") {
8834
+ yield { type: "phase", phase: "reasoning", cursor, rawEvent: event };
8835
+ } else if (step.type === "model_output") {
8836
+ yield { type: "phase", phase: "writing", cursor, rawEvent: event };
8837
+ } else if (step.type === "google_search_call") {
8838
+ if (!countedSearchSteps.has(event.index)) {
8839
+ countedSearchSteps.add(event.index);
8840
+ streamedSearches += step.arguments?.queries?.length ?? 1;
8841
+ }
8842
+ yield { type: "phase", phase: "searching", cursor, rawEvent: event };
8843
+ yield {
8844
+ type: "search",
8845
+ action: "search",
8846
+ status: "started",
8847
+ query: step.arguments?.queries?.join("; "),
8848
+ cursor
8849
+ };
8850
+ } else if (step.type === "google_search_result") {
8851
+ yield { type: "search", action: "search", status: "completed", cursor, rawEvent: event };
8852
+ }
8853
+ break;
8854
+ }
8855
+ case "step.delta":
8856
+ yield* normalizeDelta(event, cursor, (queries) => {
8857
+ if (!countedSearchSteps.has(event.index)) {
8858
+ countedSearchSteps.add(event.index);
8859
+ streamedSearches += queries;
8860
+ }
8861
+ });
8862
+ break;
8863
+ case "step.stop":
8864
+ break;
8865
+ case "interaction.completed": {
8866
+ let interaction = event.interaction;
8867
+ const finalId = interaction.id ?? interactionId;
8868
+ if (fetchFinal && finalId !== void 0 && !interaction.usage?.total_tokens) {
8869
+ try {
8870
+ interaction = await fetchFinal(finalId);
8871
+ } catch {
8872
+ }
8873
+ }
8874
+ const searches = streamedSearches > 0 ? streamedSearches : void 0;
8875
+ yield {
8876
+ type: "usage",
8877
+ usage: usageFromInteraction(interaction, searches),
8878
+ cursor,
8879
+ rawEvent: event
8880
+ };
8881
+ yield { ...doneEventFromInteraction(interaction, searches), cursor };
8882
+ break;
8883
+ }
8884
+ case "error":
8885
+ if (event.error?.code === "client_closed_request") break;
8886
+ yield {
8887
+ type: "error",
8888
+ error: {
8889
+ message: event.error?.message ?? "Gemini interaction error",
8890
+ code: event.error?.code !== void 0 ? String(event.error.code) : void 0,
8891
+ retryable: isRetryableError(new Error(event.error?.message ?? ""))
8892
+ },
8893
+ cursor,
8894
+ rawEvent: event
8895
+ };
8896
+ break;
8897
+ default: {
8898
+ const type = event.event_type ?? "";
8899
+ if (type.endsWith("error")) {
8900
+ const err = event.error;
8901
+ if (err?.code === "client_closed_request") break;
8902
+ yield {
8903
+ type: "error",
8904
+ error: { message: err?.message ?? "Gemini interaction error", retryable: false },
8905
+ cursor,
8906
+ rawEvent: event
8907
+ };
8908
+ }
8909
+ break;
8910
+ }
8911
+ }
8912
+ }
8913
+ }
8914
+ function* normalizeDelta(event, cursor, onSearchCall) {
8915
+ const delta = event.delta;
8916
+ switch (delta.type) {
8917
+ case "text": {
8918
+ const textDelta = delta;
8919
+ yield { type: "text", delta: textDelta.text, cursor, rawEvent: event };
8920
+ break;
8921
+ }
8922
+ // Citations arrive as dedicated annotation deltas in the current schema.
8923
+ case "text_annotation_delta": {
8924
+ const annotationDelta = delta;
8925
+ for (const annotation of annotationDelta.annotations ?? []) {
8926
+ const citation = annotationToCitation(annotation);
8927
+ if (citation) {
8928
+ yield { type: "citation", citation, cursor, rawEvent: event };
8929
+ }
8930
+ }
8931
+ break;
8932
+ }
8933
+ // The SDK emits "thought_summary"; some preview docs show "thought" —
8934
+ // handle both defensively.
8935
+ case "thought_summary":
8936
+ case "thought": {
8937
+ const thought = delta;
8938
+ const text3 = thought.content?.type === "text" ? thought.content.text ?? "" : "";
8939
+ if (text3.length > 0) {
8940
+ yield { type: "thinking", delta: text3, cursor, rawEvent: event };
8941
+ }
8942
+ break;
8943
+ }
8944
+ case "thought_signature":
8945
+ break;
8946
+ case "google_search_call": {
8947
+ const call = delta;
8948
+ onSearchCall(call.arguments?.queries?.length ?? 1);
8949
+ yield {
8950
+ type: "search",
8951
+ action: "search",
8952
+ status: "started",
8953
+ query: call.arguments?.queries?.join("; "),
8954
+ cursor,
8955
+ rawEvent: event
8956
+ };
8957
+ break;
8958
+ }
8959
+ case "google_search_result": {
8960
+ const result = delta;
8961
+ yield {
8962
+ type: "search",
8963
+ action: "search",
8964
+ status: "completed",
8965
+ url: result.result?.[0]?.url,
8966
+ cursor,
8967
+ rawEvent: event
8968
+ };
8969
+ break;
8970
+ }
8971
+ case "url_context_call": {
8972
+ const call = delta;
8973
+ yield {
8974
+ type: "search",
8975
+ action: "open_page",
8976
+ status: "started",
8977
+ url: call.arguments?.urls?.[0],
8978
+ cursor,
8979
+ rawEvent: event
8980
+ };
8981
+ break;
8982
+ }
8983
+ case "url_context_result":
8984
+ yield { type: "search", action: "open_page", status: "completed", cursor, rawEvent: event };
8985
+ break;
8986
+ case "code_execution_call":
8987
+ yield { type: "tool", tool: "code_interpreter", status: "started", cursor, rawEvent: event };
8988
+ break;
8989
+ case "code_execution_result":
8990
+ yield {
8991
+ type: "tool",
8992
+ tool: "code_interpreter",
8993
+ status: "completed",
8994
+ cursor,
8995
+ rawEvent: event
8996
+ };
8997
+ break;
8998
+ case "file_search_call":
8999
+ yield { type: "tool", tool: "file_search", status: "started", cursor, rawEvent: event };
9000
+ break;
9001
+ case "file_search_result":
9002
+ yield { type: "tool", tool: "file_search", status: "completed", cursor, rawEvent: event };
9003
+ break;
9004
+ case "mcp_server_tool_call":
9005
+ yield { type: "tool", tool: "mcp", status: "started", cursor, rawEvent: event };
9006
+ break;
9007
+ case "mcp_server_tool_result":
9008
+ yield { type: "tool", tool: "mcp", status: "completed", cursor, rawEvent: event };
9009
+ break;
9010
+ default:
9011
+ break;
9012
+ }
9013
+ }
9014
+ function abortError() {
9015
+ const error = new Error("The operation was aborted");
9016
+ error.name = "AbortError";
9017
+ return error;
9018
+ }
9019
+ function sleep(ms, signal) {
9020
+ return new Promise((resolve2, reject) => {
9021
+ if (signal?.aborted) {
9022
+ reject(abortError());
9023
+ return;
9024
+ }
9025
+ const timer = setTimeout(() => {
9026
+ signal?.removeEventListener("abort", onAbort);
9027
+ resolve2();
9028
+ }, ms);
9029
+ timer.unref?.();
9030
+ const onAbort = () => {
9031
+ clearTimeout(timer);
9032
+ reject(abortError());
9033
+ };
9034
+ signal?.addEventListener("abort", onAbort, { once: true });
9035
+ });
9036
+ }
9037
+ async function withRetries(fn, maxRetries, signal) {
9038
+ let attempt = 0;
9039
+ while (true) {
9040
+ try {
9041
+ return await fn();
9042
+ } catch (error) {
9043
+ const err = error instanceof Error ? error : new Error(String(error));
9044
+ if (isAbortError(err) || !isRetryableError(err) || attempt >= maxRetries) {
9045
+ throw err;
9046
+ }
9047
+ attempt += 1;
9048
+ await sleep(extractRetryAfterMs(err) ?? RESEARCH_POLL_INTERVAL_MS, signal);
9049
+ }
9050
+ }
9051
+ }
9052
+ async function* withTerminalReconciliation(events, client, jobIdHint, signal) {
9053
+ let jobId = jobIdHint;
9054
+ let sawDone = false;
9055
+ for await (const event of events) {
9056
+ if (event.type === "created" && event.jobId !== null) jobId = event.jobId;
9057
+ if (event.type === "done") sawDone = true;
9058
+ yield event;
9059
+ }
9060
+ if (sawDone || jobId === void 0) return;
9061
+ const fetchInteraction = finalInteractionFetcher(client, signal);
9062
+ while (true) {
9063
+ const interaction = await fetchInteraction(jobId);
9064
+ const status = mapInteractionStatus(interaction.status);
9065
+ yield { type: "status", status };
9066
+ if (isTerminalStatus(status)) {
9067
+ yield { type: "usage", usage: usageFromInteraction(interaction) };
9068
+ yield doneEventFromInteraction(interaction);
9069
+ return;
9070
+ }
9071
+ await sleep(RESEARCH_POLL_INTERVAL_MS, signal);
9072
+ }
9073
+ }
9074
+ function startGeminiResearch(client, options, agentId, _spec) {
9075
+ const request = buildGeminiResearchRequest(options, agentId);
9076
+ return (async function* () {
9077
+ const interaction = await withRetries(
9078
+ () => client.interactions.create(request, requestOptions(options.signal)),
9079
+ GEMINI_RESEARCH_CREATE_MAX_RETRIES,
9080
+ options.signal
9081
+ );
9082
+ if (interaction.id === void 0) {
9083
+ throw new Error(
9084
+ "Gemini interactions.create returned no interaction id \u2014 cannot stream events."
9085
+ );
9086
+ }
9087
+ yield { type: "created", jobId: interaction.id, rawEvent: interaction };
9088
+ yield { type: "status", status: mapInteractionStatus(interaction.status) };
9089
+ const stream2 = await client.interactions.get(
9090
+ interaction.id,
9091
+ { stream: true },
9092
+ requestOptions(options.signal)
9093
+ );
9094
+ for await (const event of withTerminalReconciliation(
9095
+ normalizeInteractionsStream(stream2, finalInteractionFetcher(client, options.signal)),
9096
+ client,
9097
+ interaction.id,
9098
+ options.signal
9099
+ )) {
9100
+ if (event.type === "created") continue;
9101
+ yield event;
9102
+ }
9103
+ })();
9104
+ }
9105
+ function isTerminalStatus(status) {
9106
+ return status === "completed" || status === "failed" || status === "cancelled" || status === "incomplete" || status === "budget_exceeded";
9107
+ }
9108
+ function resumeGeminiResearch(client, ref, signal) {
9109
+ return (async function* () {
9110
+ const current = await client.interactions.get(
9111
+ ref.jobId,
9112
+ void 0,
9113
+ requestOptions(signal)
9114
+ );
9115
+ const status = mapInteractionStatus(current.status);
9116
+ if (isTerminalStatus(status)) {
9117
+ yield { type: "status", status };
9118
+ const { report, citations } = extractReportFromInteraction(current);
9119
+ if (report.length > 0) {
9120
+ yield { type: "text", delta: report };
9121
+ }
9122
+ for (const citation of citations) {
9123
+ yield { type: "citation", citation };
9124
+ }
9125
+ yield { type: "usage", usage: usageFromInteraction(current) };
9126
+ yield doneEventFromInteraction(current);
9127
+ return;
9128
+ }
9129
+ const stream2 = await client.interactions.get(
9130
+ ref.jobId,
9131
+ { stream: true, ...ref.cursor !== void 0 ? { last_event_id: ref.cursor } : {} },
9132
+ requestOptions(signal)
9133
+ );
9134
+ yield* withTerminalReconciliation(
9135
+ normalizeInteractionsStream(stream2, finalInteractionFetcher(client, signal)),
9136
+ client,
9137
+ ref.jobId,
9138
+ signal
9139
+ );
9140
+ })();
9141
+ }
9142
+ async function getGeminiResearchStatus(client, ref) {
9143
+ const interaction = await client.interactions.get(
9144
+ ref.jobId
9145
+ );
9146
+ const status = mapInteractionStatus(interaction.status);
9147
+ if (!isTerminalStatus(status)) {
9148
+ return { status };
9149
+ }
9150
+ const { report, citations } = extractReportFromInteraction(interaction);
9151
+ return {
9152
+ status,
9153
+ result: {
9154
+ jobId: interaction.id,
9155
+ provider: "gemini",
9156
+ model: ref.model,
9157
+ status,
9158
+ report,
9159
+ citations,
9160
+ usage: usageFromInteraction(interaction),
9161
+ raw: interaction
9162
+ }
9163
+ };
9164
+ }
9165
+ async function cancelGeminiResearch(client, ref) {
9166
+ await client.interactions.cancel(ref.jobId);
9167
+ }
9168
+ var GEMINI_RESEARCH_CREATE_MAX_RETRIES, GEMINI_STATUS_VALUES, requestOptions, finalInteractionFetcher;
9169
+ var init_gemini_research = __esm({
9170
+ "src/providers/gemini-research.ts"() {
9171
+ "use strict";
9172
+ init_errors();
9173
+ init_retry();
9174
+ init_constants3();
9175
+ init_errors2();
9176
+ GEMINI_RESEARCH_CREATE_MAX_RETRIES = 3;
9177
+ GEMINI_STATUS_VALUES = /* @__PURE__ */ new Set([
9178
+ "queued",
9179
+ "in_progress",
9180
+ "requires_action",
9181
+ "completed",
9182
+ "failed",
9183
+ "cancelled",
9184
+ "incomplete",
9185
+ "budget_exceeded"
9186
+ ]);
9187
+ requestOptions = (signal) => ({
9188
+ timeout: GEMINI_RESEARCH_MAX_DURATION_MS,
9189
+ signal,
9190
+ // The SDK's own retries would fight llmist's retry policy.
9191
+ maxRetries: 0
9192
+ });
9193
+ finalInteractionFetcher = (client, signal) => (id) => client.interactions.get(
9194
+ id,
9195
+ void 0,
9196
+ requestOptions(signal)
9197
+ );
9198
+ }
9199
+ });
9200
+
9201
+ // src/providers/gemini-research-models.ts
9202
+ function geminiResearchAgent(params) {
9203
+ return {
9204
+ provider: "gemini",
9205
+ modelId: params.agentId,
9206
+ kind: "agent",
9207
+ displayName: params.displayName,
9208
+ // Gemini 3.1 Pro base-tier token rates; >200K-context tier is higher.
9209
+ pricing: { input: 2, output: 12, perThousandSearches: 14 },
9210
+ capabilities: {
9211
+ streaming: true,
9212
+ // The Interactions API requires background execution for deep research.
9213
+ background: true,
9214
+ resumable: true,
9215
+ followUps: true,
9216
+ // Research tools are agent-managed — the tools option is not accepted.
9217
+ tools: []
9218
+ },
9219
+ // Interactions enforces a 60-minute research cap server-side.
9220
+ maxDurationMs: GEMINI_RESEARCH_MAX_DURATION_MS,
9221
+ metadata: params.notes ? { notes: params.notes } : void 0
9222
+ };
9223
+ }
9224
+ function isGeminiResearchModel(agentId) {
9225
+ return byId.has(agentId);
9226
+ }
9227
+ var geminiResearchModels, byId;
9228
+ var init_gemini_research_models = __esm({
9229
+ "src/providers/gemini-research-models.ts"() {
9230
+ "use strict";
9231
+ init_constants3();
9232
+ geminiResearchModels = [
9233
+ geminiResearchAgent({
9234
+ agentId: "deep-research-preview-04-2026",
9235
+ displayName: "Gemini Deep Research (preview 04-2026)",
9236
+ notes: "Speed-optimized deep research agent on a Gemini 3.1 Pro core."
9237
+ }),
9238
+ geminiResearchAgent({
9239
+ agentId: "deep-research-max-preview-04-2026",
9240
+ displayName: "Gemini Deep Research Max (preview 04-2026)",
9241
+ notes: "Maximum-comprehensiveness variant; roughly 2x the searches and tokens per run."
9242
+ }),
9243
+ geminiResearchAgent({
9244
+ agentId: "deep-research-pro-preview-12-2025",
9245
+ displayName: "Gemini Deep Research Pro (preview 12-2025)",
9246
+ notes: "Original deep research agent (Gemini 3 Pro core); prefer the 04-2026 agents."
9247
+ })
9248
+ ];
9249
+ byId = new Map(geminiResearchModels.map((spec) => [spec.modelId, spec]));
9250
+ }
9251
+ });
9252
+
8604
9253
  // src/providers/gemini-speech-models.ts
8605
9254
  function getGeminiSpeechModelSpec(modelId) {
8606
9255
  return geminiSpeechModels.find((m) => m.modelId === modelId);
@@ -8798,6 +9447,8 @@ var init_gemini = __esm({
8798
9447
  init_gemini_cache_manager();
8799
9448
  init_gemini_image_models();
8800
9449
  init_gemini_models();
9450
+ init_gemini_research();
9451
+ init_gemini_research_models();
8801
9452
  init_gemini_speech_models();
8802
9453
  init_utils();
8803
9454
  GEMINI3_PRO_THINKING_LEVEL = {
@@ -9017,6 +9668,27 @@ var init_gemini = __esm({
9017
9668
  format: spec?.defaultFormat ?? "wav"
9018
9669
  };
9019
9670
  }
9671
+ // =========================================================================
9672
+ // Deep Research (Interactions API)
9673
+ // =========================================================================
9674
+ getResearchModelSpecs() {
9675
+ return geminiResearchModels;
9676
+ }
9677
+ supportsResearch(agentId) {
9678
+ return isGeminiResearchModel(agentId);
9679
+ }
9680
+ startResearch(options, descriptor, spec) {
9681
+ return startGeminiResearch(this.client, options, descriptor.name, spec);
9682
+ }
9683
+ resumeResearch(ref, signal) {
9684
+ return resumeGeminiResearch(this.client, ref, signal);
9685
+ }
9686
+ getResearchStatus(ref) {
9687
+ return getGeminiResearchStatus(this.client, ref);
9688
+ }
9689
+ cancelResearch(ref) {
9690
+ return cancelGeminiResearch(this.client, ref);
9691
+ }
9020
9692
  buildApiRequest(options, descriptor, _spec, messages) {
9021
9693
  const contents = this.convertMessagesToContents(messages);
9022
9694
  return this.buildApiRequestFromContents(options, descriptor, _spec, contents, null, 0);
@@ -10243,17 +10915,17 @@ var init_openai_compatible_provider = __esm({
10243
10915
  async executeStreamRequest(payload, signal) {
10244
10916
  const client = this.client;
10245
10917
  const headers = this.getCustomHeaders();
10246
- const requestOptions = {};
10918
+ const requestOptions3 = {};
10247
10919
  if (signal) {
10248
- requestOptions.signal = signal;
10920
+ requestOptions3.signal = signal;
10249
10921
  }
10250
10922
  if (Object.keys(headers).length > 0) {
10251
- requestOptions.headers = headers;
10923
+ requestOptions3.headers = headers;
10252
10924
  }
10253
10925
  try {
10254
10926
  const stream2 = await client.chat.completions.create(
10255
10927
  payload,
10256
- Object.keys(requestOptions).length > 0 ? requestOptions : void 0
10928
+ Object.keys(requestOptions3).length > 0 ? requestOptions3 : void 0
10257
10929
  );
10258
10930
  return stream2;
10259
10931
  } catch (error) {
@@ -11130,53 +11802,596 @@ var init_openai_models = __esm({
11130
11802
  }
11131
11803
  });
11132
11804
 
11133
- // src/providers/openai-speech-models.ts
11134
- function getOpenAISpeechModelSpec(modelId) {
11135
- return openaiSpeechModels.find((m) => m.modelId === modelId);
11805
+ // src/providers/openai-research.ts
11806
+ function buildTools(options) {
11807
+ const tools = [];
11808
+ for (const tool of options.tools ?? []) {
11809
+ switch (tool.type) {
11810
+ case "web_search":
11811
+ tools.push({ type: "web_search_preview" });
11812
+ break;
11813
+ case "file_search":
11814
+ if (tool.vectorStoreIds.length > OPENAI_FILE_SEARCH_MAX_VECTOR_STORES) {
11815
+ throw new ResearchValidationError(
11816
+ `OpenAI file_search accepts at most ${OPENAI_FILE_SEARCH_MAX_VECTOR_STORES} vector stores (got ${tool.vectorStoreIds.length}).`
11817
+ );
11818
+ }
11819
+ tools.push({ type: "file_search", vector_store_ids: tool.vectorStoreIds });
11820
+ break;
11821
+ case "mcp":
11822
+ tools.push({
11823
+ type: "mcp",
11824
+ server_label: tool.serverLabel,
11825
+ server_url: tool.serverUrl,
11826
+ require_approval: tool.requireApproval ?? "never"
11827
+ });
11828
+ break;
11829
+ case "code_interpreter":
11830
+ tools.push({ type: "code_interpreter", container: { type: "auto" } });
11831
+ break;
11832
+ }
11833
+ }
11834
+ return tools;
11136
11835
  }
11137
- function isOpenAISpeechModel(modelId) {
11138
- return openaiSpeechModels.some((m) => m.modelId === modelId);
11836
+ function buildResponsesResearchRequest(options, spec, modelId) {
11837
+ const tools = buildTools(options);
11838
+ const hasDataSource = (options.tools ?? []).some(
11839
+ (tool) => ["web_search", "file_search", "mcp"].includes(tool.type)
11840
+ );
11841
+ if (!hasDataSource) {
11842
+ throw new ResearchValidationError(
11843
+ "OpenAI deep research requires at least one data-source tool (web_search, file_search, or mcp)."
11844
+ );
11845
+ }
11846
+ const background = options.background ?? spec?.capabilities.background ?? true;
11847
+ const input = options.systemPrompt ? `${options.systemPrompt}
11848
+
11849
+ ${options.query}` : options.query;
11850
+ const request = {
11851
+ model: modelId,
11852
+ input,
11853
+ background,
11854
+ // Background mode requires stored responses; polling/resume need them too.
11855
+ ...background ? { store: true } : {},
11856
+ reasoning: {
11857
+ summary: "auto",
11858
+ ...options.reasoning?.effort && options.reasoning.effort !== "none" ? { effort: mapEffort(options.reasoning.effort) } : {}
11859
+ },
11860
+ tools,
11861
+ ...options.maxToolCalls !== void 0 ? { max_tool_calls: options.maxToolCalls } : {},
11862
+ ...options.extra ?? {}
11863
+ };
11864
+ return request;
11139
11865
  }
11140
- function calculateOpenAISpeechCost(modelId, characterCount, estimatedMinutes) {
11141
- const spec = getOpenAISpeechModelSpec(modelId);
11142
- if (!spec) return void 0;
11143
- if (spec.pricing.perCharacter !== void 0) {
11144
- return characterCount * spec.pricing.perCharacter;
11866
+ function mapEffort(effort) {
11867
+ switch (effort) {
11868
+ case "maximum":
11869
+ return "xhigh";
11870
+ case "low":
11871
+ case "medium":
11872
+ case "high":
11873
+ return effort;
11874
+ default:
11875
+ return "medium";
11145
11876
  }
11146
- if (spec.pricing.perMinute !== void 0 && estimatedMinutes !== void 0) {
11147
- return estimatedMinutes * spec.pricing.perMinute;
11877
+ }
11878
+ function extractReportFromResponse(response) {
11879
+ const citations = [];
11880
+ const parts = [];
11881
+ for (const item of response.output ?? []) {
11882
+ if (item.type !== "message") continue;
11883
+ for (const content of item.content ?? []) {
11884
+ if (content.type !== "output_text") continue;
11885
+ parts.push(content.text);
11886
+ for (const annotation of content.annotations ?? []) {
11887
+ const citation = annotationToCitation2(annotation);
11888
+ if (citation) citations.push(citation);
11889
+ }
11890
+ }
11148
11891
  }
11149
- if (spec.pricing.perMinute !== void 0) {
11150
- const approxMinutes = characterCount / 750;
11151
- return approxMinutes * spec.pricing.perMinute;
11892
+ return { report: parts.join(""), citations };
11893
+ }
11894
+ function annotationToCitation2(annotation) {
11895
+ if (typeof annotation !== "object" || annotation === null) return void 0;
11896
+ const a = annotation;
11897
+ if (a.type !== "url_citation" || typeof a.url !== "string") return void 0;
11898
+ return {
11899
+ url: a.url,
11900
+ title: typeof a.title === "string" ? a.title : void 0,
11901
+ startIndex: typeof a.start_index === "number" ? a.start_index : void 0,
11902
+ endIndex: typeof a.end_index === "number" ? a.end_index : void 0
11903
+ };
11904
+ }
11905
+ function countSearches(response) {
11906
+ let searches = 0;
11907
+ for (const item of response.output ?? []) {
11908
+ if (item.type === "web_search_call") searches += 1;
11152
11909
  }
11153
- return void 0;
11910
+ return searches;
11154
11911
  }
11155
- var OPENAI_TTS_VOICES, OPENAI_TTS_EXTENDED_VOICES, OPENAI_TTS_FORMATS, openaiSpeechModels;
11156
- var init_openai_speech_models = __esm({
11157
- "src/providers/openai-speech-models.ts"() {
11158
- "use strict";
11159
- OPENAI_TTS_VOICES = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"];
11160
- OPENAI_TTS_EXTENDED_VOICES = [
11161
- ...OPENAI_TTS_VOICES,
11162
- "ash",
11163
- "ballad",
11164
- "coral",
11165
- "sage",
11166
- "verse"
11167
- ];
11168
- OPENAI_TTS_FORMATS = ["mp3", "opus", "aac", "flac", "wav", "pcm"];
11169
- openaiSpeechModels = [
11170
- // Standard TTS models (character-based pricing)
11171
- {
11172
- provider: "openai",
11173
- modelId: "tts-1",
11174
- displayName: "TTS-1",
11175
- pricing: {
11176
- // $15 per 1M characters = $0.000015 per character
11177
- perCharacter: 15e-6
11178
- },
11179
- voices: [...OPENAI_TTS_VOICES],
11912
+ function usageFromResponse(response) {
11913
+ const usage = response.usage;
11914
+ return {
11915
+ inputTokens: usage?.input_tokens ?? 0,
11916
+ outputTokens: usage?.output_tokens ?? 0,
11917
+ totalTokens: usage?.total_tokens ?? 0,
11918
+ cachedInputTokens: usage?.input_tokens_details?.cached_tokens,
11919
+ reasoningTokens: usage?.output_tokens_details?.reasoning_tokens,
11920
+ searches: countSearches(response)
11921
+ };
11922
+ }
11923
+ function mapResponseStatus(status) {
11924
+ return status && STATUS_MAP[status] || "in_progress";
11925
+ }
11926
+ function doneEventFromResponse(response) {
11927
+ const { report, citations } = extractReportFromResponse(response);
11928
+ return {
11929
+ type: "done",
11930
+ result: {
11931
+ status: mapResponseStatus(response.status),
11932
+ report,
11933
+ citations,
11934
+ usage: usageFromResponse(response),
11935
+ raw: response
11936
+ }
11937
+ };
11938
+ }
11939
+ async function* normalizeResponsesStream(stream2) {
11940
+ const searchStarted = /* @__PURE__ */ new Set();
11941
+ for await (const event of stream2) {
11942
+ const cursor = String(event.sequence_number);
11943
+ switch (event.type) {
11944
+ case "response.created":
11945
+ yield { type: "created", jobId: event.response.id, cursor, rawEvent: event };
11946
+ yield { type: "status", status: mapResponseStatus(event.response.status), cursor };
11947
+ break;
11948
+ case "response.queued":
11949
+ yield { type: "status", status: "queued", cursor, rawEvent: event };
11950
+ break;
11951
+ case "response.in_progress":
11952
+ yield { type: "status", status: "in_progress", cursor, rawEvent: event };
11953
+ break;
11954
+ case "response.output_item.added": {
11955
+ const item = event.item;
11956
+ if (item.type === "reasoning") {
11957
+ yield { type: "phase", phase: "reasoning", cursor, rawEvent: event };
11958
+ } else if (item.type === "web_search_call") {
11959
+ const action = "action" in item ? item.action : void 0;
11960
+ searchStarted.add(item.id);
11961
+ yield {
11962
+ type: "search",
11963
+ action: action?.type ?? "search",
11964
+ status: "started",
11965
+ query: action && "query" in action ? action.query ?? void 0 : void 0,
11966
+ url: action && "url" in action ? action.url ?? void 0 : void 0,
11967
+ cursor,
11968
+ rawEvent: event
11969
+ };
11970
+ } else if (item.type === "code_interpreter_call") {
11971
+ yield {
11972
+ type: "tool",
11973
+ tool: "code_interpreter",
11974
+ status: "started",
11975
+ cursor,
11976
+ rawEvent: event
11977
+ };
11978
+ } else if (item.type === "file_search_call") {
11979
+ yield { type: "tool", tool: "file_search", status: "started", cursor, rawEvent: event };
11980
+ } else if (item.type === "mcp_call") {
11981
+ yield { type: "tool", tool: "mcp", status: "started", cursor, rawEvent: event };
11982
+ } else if (item.type === "message") {
11983
+ yield { type: "phase", phase: "writing", cursor, rawEvent: event };
11984
+ }
11985
+ break;
11986
+ }
11987
+ case "response.web_search_call.in_progress":
11988
+ case "response.web_search_call.searching":
11989
+ if (!searchStarted.has(event.item_id)) {
11990
+ searchStarted.add(event.item_id);
11991
+ yield { type: "search", action: "search", status: "started", cursor, rawEvent: event };
11992
+ }
11993
+ break;
11994
+ case "response.web_search_call.completed":
11995
+ yield { type: "search", action: "search", status: "completed", cursor, rawEvent: event };
11996
+ break;
11997
+ case "response.code_interpreter_call.completed":
11998
+ yield {
11999
+ type: "tool",
12000
+ tool: "code_interpreter",
12001
+ status: "completed",
12002
+ cursor,
12003
+ rawEvent: event
12004
+ };
12005
+ break;
12006
+ case "response.file_search_call.completed":
12007
+ yield { type: "tool", tool: "file_search", status: "completed", cursor, rawEvent: event };
12008
+ break;
12009
+ case "response.mcp_call.completed":
12010
+ yield { type: "tool", tool: "mcp", status: "completed", cursor, rawEvent: event };
12011
+ break;
12012
+ case "response.reasoning_summary_text.delta":
12013
+ yield { type: "thinking", delta: event.delta, cursor, rawEvent: event };
12014
+ break;
12015
+ case "response.output_text.delta":
12016
+ yield { type: "text", delta: event.delta, cursor, rawEvent: event };
12017
+ break;
12018
+ case "response.output_text.annotation.added": {
12019
+ const citation = annotationToCitation2(event.annotation);
12020
+ if (citation) {
12021
+ yield { type: "citation", citation, cursor, rawEvent: event };
12022
+ }
12023
+ break;
12024
+ }
12025
+ case "response.completed":
12026
+ case "response.failed":
12027
+ case "response.incomplete": {
12028
+ const response = event.response;
12029
+ yield { type: "usage", usage: usageFromResponse(response), cursor, rawEvent: event };
12030
+ if (event.type === "response.failed") {
12031
+ const message = response.error?.message ?? "OpenAI research run failed";
12032
+ yield {
12033
+ type: "error",
12034
+ error: { message, code: response.error?.code ?? void 0, retryable: false },
12035
+ cursor
12036
+ };
12037
+ }
12038
+ yield { ...doneEventFromResponse(response), cursor };
12039
+ break;
12040
+ }
12041
+ case "error": {
12042
+ const err = new Error(event.message ?? "OpenAI stream error");
12043
+ yield {
12044
+ type: "error",
12045
+ error: {
12046
+ message: event.message ?? "OpenAI stream error",
12047
+ code: event.code ?? void 0,
12048
+ retryable: isRetryableError(err)
12049
+ },
12050
+ cursor,
12051
+ rawEvent: event
12052
+ };
12053
+ break;
12054
+ }
12055
+ default: {
12056
+ const type = event.type;
12057
+ if (type === "response.cancelled") {
12058
+ const response = event.response;
12059
+ yield { type: "usage", usage: usageFromResponse(response), cursor, rawEvent: event };
12060
+ yield { ...doneEventFromResponse(response), cursor };
12061
+ }
12062
+ break;
12063
+ }
12064
+ }
12065
+ }
12066
+ }
12067
+ function sleep2(ms, signal) {
12068
+ return new Promise((resolve2, reject) => {
12069
+ if (signal?.aborted) {
12070
+ reject(abortError2());
12071
+ return;
12072
+ }
12073
+ const timer = setTimeout(() => {
12074
+ signal?.removeEventListener("abort", onAbort);
12075
+ resolve2();
12076
+ }, ms);
12077
+ timer.unref?.();
12078
+ const onAbort = () => {
12079
+ clearTimeout(timer);
12080
+ reject(abortError2());
12081
+ };
12082
+ signal?.addEventListener("abort", onAbort, { once: true });
12083
+ });
12084
+ }
12085
+ function abortError2() {
12086
+ const error = new Error("The operation was aborted");
12087
+ error.name = "AbortError";
12088
+ return error;
12089
+ }
12090
+ async function withRetries2(fn, maxRetries, signal) {
12091
+ let attempt = 0;
12092
+ while (true) {
12093
+ try {
12094
+ return await fn();
12095
+ } catch (error) {
12096
+ const err = error instanceof Error ? error : new Error(String(error));
12097
+ if (isAbortError(err) || !isRetryableError(err) || attempt >= maxRetries) {
12098
+ throw err;
12099
+ }
12100
+ attempt += 1;
12101
+ const retryAfter = extractRetryAfterMs(err);
12102
+ await sleep2(retryAfter ?? RESEARCH_POLL_INTERVAL_MS, signal);
12103
+ }
12104
+ }
12105
+ }
12106
+ function startOpenAIResearch(client, options, modelId, spec) {
12107
+ const request = buildResponsesResearchRequest(options, spec, modelId);
12108
+ const streaming = spec?.capabilities.streaming ?? true;
12109
+ if (streaming) {
12110
+ return (async function* () {
12111
+ const stream2 = await withRetries2(
12112
+ () => client.responses.create(
12113
+ { ...request, stream: true },
12114
+ requestOptions2(options.signal)
12115
+ ),
12116
+ OPENAI_RESEARCH_CREATE_MAX_RETRIES,
12117
+ options.signal
12118
+ );
12119
+ yield* normalizeResponsesStream(stream2);
12120
+ })();
12121
+ }
12122
+ return (async function* () {
12123
+ const created = await withRetries2(
12124
+ () => client.responses.create(
12125
+ { ...request, background: true, store: true, stream: false },
12126
+ requestOptions2(options.signal)
12127
+ ),
12128
+ OPENAI_RESEARCH_CREATE_MAX_RETRIES,
12129
+ options.signal
12130
+ );
12131
+ yield { type: "created", jobId: created.id, rawEvent: created };
12132
+ yield* pollResponseToCompletion(client, created.id, created, options.signal);
12133
+ })();
12134
+ }
12135
+ function* emitTerminalResponse(response) {
12136
+ const status = mapResponseStatus(response.status);
12137
+ const { report, citations } = extractReportFromResponse(response);
12138
+ if (report.length > 0) {
12139
+ yield { type: "text", delta: report };
12140
+ }
12141
+ for (const citation of citations) {
12142
+ yield { type: "citation", citation };
12143
+ }
12144
+ yield { type: "usage", usage: usageFromResponse(response) };
12145
+ if (status === "failed") {
12146
+ yield {
12147
+ type: "error",
12148
+ error: {
12149
+ message: response.error?.message ?? "OpenAI research run failed",
12150
+ code: response.error?.code ?? void 0,
12151
+ retryable: false
12152
+ }
12153
+ };
12154
+ }
12155
+ yield doneEventFromResponse(response);
12156
+ }
12157
+ async function* pollResponseToCompletion(client, responseId, initial, signal) {
12158
+ let interval = RESEARCH_POLL_INTERVAL_MS;
12159
+ let current = initial;
12160
+ while (true) {
12161
+ const status = mapResponseStatus(current?.status);
12162
+ yield { type: "status", status, rawEvent: current };
12163
+ if (current && isTerminalStatus2(status)) {
12164
+ yield* emitTerminalResponse(current);
12165
+ return;
12166
+ }
12167
+ await sleep2(interval, signal);
12168
+ interval = Math.min(interval * RESEARCH_POLL_BACKOFF_FACTOR, RESEARCH_POLL_MAX_INTERVAL_MS);
12169
+ current = await withRetries2(
12170
+ () => client.responses.retrieve(responseId, {}, requestOptions2(signal)),
12171
+ OPENAI_RESEARCH_POLL_MAX_RETRIES,
12172
+ signal
12173
+ );
12174
+ }
12175
+ }
12176
+ function isTerminalStatus2(status) {
12177
+ return status === "completed" || status === "failed" || status === "cancelled" || status === "incomplete";
12178
+ }
12179
+ function resumeOpenAIResearch(client, ref, spec, signal) {
12180
+ const streaming = spec?.capabilities.streaming ?? true;
12181
+ if (streaming) {
12182
+ return (async function* () {
12183
+ const current = await withRetries2(
12184
+ () => client.responses.retrieve(ref.jobId, {}, requestOptions2(signal)),
12185
+ OPENAI_RESEARCH_POLL_MAX_RETRIES,
12186
+ signal
12187
+ );
12188
+ if (isTerminalStatus2(mapResponseStatus(current.status))) {
12189
+ yield { type: "status", status: mapResponseStatus(current.status) };
12190
+ yield* emitTerminalResponse(current);
12191
+ return;
12192
+ }
12193
+ const stream2 = await client.responses.retrieve(
12194
+ ref.jobId,
12195
+ {
12196
+ stream: true,
12197
+ ...ref.cursor !== void 0 ? { starting_after: Number(ref.cursor) } : {}
12198
+ },
12199
+ requestOptions2(signal)
12200
+ );
12201
+ yield* normalizeResponsesStream(stream2);
12202
+ })();
12203
+ }
12204
+ return pollResponseToCompletion(client, ref.jobId, void 0, signal);
12205
+ }
12206
+ async function getOpenAIResearchStatus(client, ref) {
12207
+ const response = await client.responses.retrieve(ref.jobId, {});
12208
+ const status = mapResponseStatus(response.status);
12209
+ if (!isTerminalStatus2(status)) {
12210
+ return { status };
12211
+ }
12212
+ const { report, citations } = extractReportFromResponse(response);
12213
+ return {
12214
+ status,
12215
+ result: {
12216
+ jobId: response.id,
12217
+ provider: "openai",
12218
+ model: ref.model,
12219
+ status,
12220
+ report,
12221
+ citations,
12222
+ usage: usageFromResponse(response),
12223
+ raw: response
12224
+ }
12225
+ };
12226
+ }
12227
+ async function cancelOpenAIResearch(client, ref) {
12228
+ await client.responses.cancel(ref.jobId);
12229
+ }
12230
+ var OPENAI_FILE_SEARCH_MAX_VECTOR_STORES, OPENAI_RESEARCH_CREATE_MAX_RETRIES, OPENAI_RESEARCH_POLL_MAX_RETRIES, STATUS_MAP, requestOptions2;
12231
+ var init_openai_research = __esm({
12232
+ "src/providers/openai-research.ts"() {
12233
+ "use strict";
12234
+ init_errors();
12235
+ init_retry();
12236
+ init_constants3();
12237
+ init_errors2();
12238
+ OPENAI_FILE_SEARCH_MAX_VECTOR_STORES = 2;
12239
+ OPENAI_RESEARCH_CREATE_MAX_RETRIES = 3;
12240
+ OPENAI_RESEARCH_POLL_MAX_RETRIES = 5;
12241
+ STATUS_MAP = {
12242
+ queued: "queued",
12243
+ in_progress: "in_progress",
12244
+ completed: "completed",
12245
+ failed: "failed",
12246
+ cancelled: "cancelled",
12247
+ incomplete: "incomplete"
12248
+ };
12249
+ requestOptions2 = (signal) => ({
12250
+ timeout: OPENAI_RESEARCH_HTTP_TIMEOUT_MS,
12251
+ signal
12252
+ });
12253
+ }
12254
+ });
12255
+
12256
+ // src/providers/openai-research-models.ts
12257
+ function o3DeepResearch(modelId) {
12258
+ return {
12259
+ provider: "openai",
12260
+ modelId,
12261
+ kind: "model",
12262
+ displayName: "o3 Deep Research",
12263
+ contextWindow: 2e5,
12264
+ maxOutputTokens: 1e5,
12265
+ pricing: { input: 10, cachedInput: 2.5, output: 40, perThousandSearches: 10 },
12266
+ capabilities: {
12267
+ streaming: true,
12268
+ background: true,
12269
+ resumable: true,
12270
+ tools: DEEP_RESEARCH_TOOLS
12271
+ },
12272
+ requiredTools: [{ type: "web_search" }],
12273
+ metadata: { releaseDate: "2025-06-26", ...DEEP_RESEARCH_SHUTDOWN }
12274
+ };
12275
+ }
12276
+ function o4MiniDeepResearch(modelId) {
12277
+ return {
12278
+ provider: "openai",
12279
+ modelId,
12280
+ kind: "model",
12281
+ displayName: "o4-mini Deep Research",
12282
+ contextWindow: 2e5,
12283
+ maxOutputTokens: 1e5,
12284
+ pricing: { input: 2, cachedInput: 0.5, output: 8, perThousandSearches: 10 },
12285
+ capabilities: {
12286
+ streaming: true,
12287
+ background: true,
12288
+ resumable: true,
12289
+ tools: DEEP_RESEARCH_TOOLS
12290
+ },
12291
+ requiredTools: [{ type: "web_search" }],
12292
+ metadata: { releaseDate: "2025-06-26", ...DEEP_RESEARCH_SHUTDOWN }
12293
+ };
12294
+ }
12295
+ function getOpenAIResearchModelSpec(modelId) {
12296
+ return byId2.get(modelId);
12297
+ }
12298
+ function isOpenAIResearchModel(modelId) {
12299
+ return byId2.has(modelId);
12300
+ }
12301
+ var DEEP_RESEARCH_TOOLS, DEEP_RESEARCH_SHUTDOWN, openaiResearchModels, byId2;
12302
+ var init_openai_research_models = __esm({
12303
+ "src/providers/openai-research-models.ts"() {
12304
+ "use strict";
12305
+ DEEP_RESEARCH_TOOLS = [
12306
+ "web_search",
12307
+ "file_search",
12308
+ "mcp",
12309
+ "code_interpreter"
12310
+ ];
12311
+ DEEP_RESEARCH_SHUTDOWN = {
12312
+ deprecationDate: "2026-04-22",
12313
+ shutdownDate: "2026-07-23",
12314
+ replacement: "gpt-5.5-pro"
12315
+ };
12316
+ openaiResearchModels = [
12317
+ o3DeepResearch("o3-deep-research"),
12318
+ o3DeepResearch("o3-deep-research-2025-06-26"),
12319
+ o4MiniDeepResearch("o4-mini-deep-research"),
12320
+ o4MiniDeepResearch("o4-mini-deep-research-2025-06-26"),
12321
+ {
12322
+ provider: "openai",
12323
+ modelId: "gpt-5.5-pro",
12324
+ kind: "model",
12325
+ displayName: "GPT-5.5 Pro (research)",
12326
+ contextWindow: 105e4,
12327
+ maxOutputTokens: 128e3,
12328
+ pricing: { input: 30, output: 180, perThousandSearches: 10 },
12329
+ capabilities: {
12330
+ // No live streaming — runs execute via background create + poll.
12331
+ streaming: false,
12332
+ background: true,
12333
+ // Resumable in the attach sense: the poll loop re-attaches by job id.
12334
+ resumable: true,
12335
+ tools: DEEP_RESEARCH_TOOLS
12336
+ },
12337
+ requiredTools: [{ type: "web_search" }],
12338
+ metadata: {
12339
+ releaseDate: "2026-04-24",
12340
+ notes: "Designated replacement for the o3/o4-mini deep research models."
12341
+ }
12342
+ }
12343
+ ];
12344
+ byId2 = new Map(openaiResearchModels.map((spec) => [spec.modelId, spec]));
12345
+ }
12346
+ });
12347
+
12348
+ // src/providers/openai-speech-models.ts
12349
+ function getOpenAISpeechModelSpec(modelId) {
12350
+ return openaiSpeechModels.find((m) => m.modelId === modelId);
12351
+ }
12352
+ function isOpenAISpeechModel(modelId) {
12353
+ return openaiSpeechModels.some((m) => m.modelId === modelId);
12354
+ }
12355
+ function calculateOpenAISpeechCost(modelId, characterCount, estimatedMinutes) {
12356
+ const spec = getOpenAISpeechModelSpec(modelId);
12357
+ if (!spec) return void 0;
12358
+ if (spec.pricing.perCharacter !== void 0) {
12359
+ return characterCount * spec.pricing.perCharacter;
12360
+ }
12361
+ if (spec.pricing.perMinute !== void 0 && estimatedMinutes !== void 0) {
12362
+ return estimatedMinutes * spec.pricing.perMinute;
12363
+ }
12364
+ if (spec.pricing.perMinute !== void 0) {
12365
+ const approxMinutes = characterCount / 750;
12366
+ return approxMinutes * spec.pricing.perMinute;
12367
+ }
12368
+ return void 0;
12369
+ }
12370
+ var OPENAI_TTS_VOICES, OPENAI_TTS_EXTENDED_VOICES, OPENAI_TTS_FORMATS, openaiSpeechModels;
12371
+ var init_openai_speech_models = __esm({
12372
+ "src/providers/openai-speech-models.ts"() {
12373
+ "use strict";
12374
+ OPENAI_TTS_VOICES = ["alloy", "echo", "fable", "onyx", "nova", "shimmer"];
12375
+ OPENAI_TTS_EXTENDED_VOICES = [
12376
+ ...OPENAI_TTS_VOICES,
12377
+ "ash",
12378
+ "ballad",
12379
+ "coral",
12380
+ "sage",
12381
+ "verse"
12382
+ ];
12383
+ OPENAI_TTS_FORMATS = ["mp3", "opus", "aac", "flac", "wav", "pcm"];
12384
+ openaiSpeechModels = [
12385
+ // Standard TTS models (character-based pricing)
12386
+ {
12387
+ provider: "openai",
12388
+ modelId: "tts-1",
12389
+ displayName: "TTS-1",
12390
+ pricing: {
12391
+ // $15 per 1M characters = $0.000015 per character
12392
+ perCharacter: 15e-6
12393
+ },
12394
+ voices: [...OPENAI_TTS_VOICES],
11180
12395
  formats: OPENAI_TTS_FORMATS,
11181
12396
  maxInputLength: 4096,
11182
12397
  defaultVoice: "alloy",
@@ -11285,6 +12500,8 @@ var init_openai = __esm({
11285
12500
  init_constants2();
11286
12501
  init_openai_image_models();
11287
12502
  init_openai_models();
12503
+ init_openai_research();
12504
+ init_openai_research_models();
11288
12505
  init_openai_speech_models();
11289
12506
  init_utils();
11290
12507
  ROLE_MAP2 = {
@@ -11388,6 +12605,32 @@ var init_openai = __esm({
11388
12605
  format: format2
11389
12606
  };
11390
12607
  }
12608
+ // =========================================================================
12609
+ // Deep Research (Responses API)
12610
+ // =========================================================================
12611
+ getResearchModelSpecs() {
12612
+ return openaiResearchModels;
12613
+ }
12614
+ supportsResearch(modelId) {
12615
+ return isOpenAIResearchModel(modelId);
12616
+ }
12617
+ startResearch(options, descriptor, spec) {
12618
+ return startOpenAIResearch(this.client, options, descriptor.name, spec);
12619
+ }
12620
+ resumeResearch(ref, signal) {
12621
+ return resumeOpenAIResearch(
12622
+ this.client,
12623
+ ref,
12624
+ getOpenAIResearchModelSpec(ref.model),
12625
+ signal
12626
+ );
12627
+ }
12628
+ getResearchStatus(ref) {
12629
+ return getOpenAIResearchStatus(this.client, ref);
12630
+ }
12631
+ cancelResearch(ref) {
12632
+ return cancelOpenAIResearch(this.client, ref);
12633
+ }
11391
12634
  buildApiRequest(options, descriptor, spec, messages) {
11392
12635
  const { maxTokens, temperature, topP, stopSequences, extra, reasoning } = options;
11393
12636
  const supportsTemperature = spec?.metadata?.supportsTemperature !== false;
@@ -12115,6 +13358,217 @@ var init_openrouter_models = __esm({
12115
13358
  }
12116
13359
  });
12117
13360
 
13361
+ // src/providers/openrouter-research.ts
13362
+ function buildOpenRouterResearchMessages(options) {
13363
+ if (options.background === true) {
13364
+ throw new ResearchValidationError(
13365
+ "OpenRouter has no background mode for research runs \u2014 the stream must stay open."
13366
+ );
13367
+ }
13368
+ if (options.tools !== void 0 && options.tools.length > 0) {
13369
+ throw new ResearchValidationError(
13370
+ "OpenRouter research models manage their tools server-side \u2014 omit the tools option."
13371
+ );
13372
+ }
13373
+ const messages = [];
13374
+ if (options.systemPrompt) {
13375
+ messages.push({ role: "system", content: options.systemPrompt });
13376
+ }
13377
+ messages.push({ role: "user", content: options.query });
13378
+ return { messages };
13379
+ }
13380
+ function citationKey(citation) {
13381
+ return `${citation.url}#${citation.startIndex ?? ""}`;
13382
+ }
13383
+ function mapFinishReason(reason) {
13384
+ switch (reason) {
13385
+ case "length":
13386
+ return "incomplete";
13387
+ case "error":
13388
+ return "failed";
13389
+ default:
13390
+ return "completed";
13391
+ }
13392
+ }
13393
+ async function* normalizeOpenRouterResearchStream(stream2) {
13394
+ let createdEmitted = false;
13395
+ const seenCitations = /* @__PURE__ */ new Set();
13396
+ const seenCitationUrls = /* @__PURE__ */ new Set();
13397
+ const bufferedLegacyCitations = [];
13398
+ let usage;
13399
+ let terminalStatus;
13400
+ let lastRaw;
13401
+ for await (const chunk of stream2) {
13402
+ lastRaw = chunk;
13403
+ if (!createdEmitted) {
13404
+ createdEmitted = true;
13405
+ yield { type: "created", jobId: null, rawEvent: chunk };
13406
+ yield { type: "status", status: "in_progress" };
13407
+ }
13408
+ const choice = chunk.choices?.[0];
13409
+ const delta = choice?.delta ?? {};
13410
+ const detailTexts = (delta.reasoning_details ?? []).filter((detail) => typeof detail.text === "string" && detail.text.length > 0).map((detail) => detail.text);
13411
+ if (detailTexts.length > 0) {
13412
+ for (const text3 of detailTexts) {
13413
+ yield { type: "thinking", delta: text3, rawEvent: chunk };
13414
+ }
13415
+ } else if (typeof delta.reasoning === "string" && delta.reasoning.length > 0) {
13416
+ yield { type: "thinking", delta: delta.reasoning, rawEvent: chunk };
13417
+ }
13418
+ if (typeof delta.content === "string" && delta.content.length > 0) {
13419
+ yield { type: "text", delta: delta.content, rawEvent: chunk };
13420
+ }
13421
+ const annotationSources = [
13422
+ ...delta.annotations ?? [],
13423
+ ...choice?.message?.annotations ?? []
13424
+ ];
13425
+ for (const annotation of annotationSources) {
13426
+ if (annotation.type !== "url_citation" || !annotation.url_citation?.url) continue;
13427
+ const citation = {
13428
+ url: annotation.url_citation.url,
13429
+ title: annotation.url_citation.title,
13430
+ content: annotation.url_citation.content,
13431
+ startIndex: annotation.url_citation.start_index,
13432
+ endIndex: annotation.url_citation.end_index
13433
+ };
13434
+ const key = citationKey(citation);
13435
+ if (!seenCitations.has(key)) {
13436
+ seenCitations.add(key);
13437
+ seenCitationUrls.add(citation.url);
13438
+ yield { type: "citation", citation, rawEvent: chunk };
13439
+ }
13440
+ }
13441
+ const legacyCitations = chunk.citations;
13442
+ if (Array.isArray(legacyCitations)) {
13443
+ for (const url of legacyCitations) {
13444
+ bufferedLegacyCitations.push({ url, rawEvent: chunk });
13445
+ }
13446
+ }
13447
+ if (choice?.finish_reason) {
13448
+ terminalStatus = mapFinishReason(choice.finish_reason);
13449
+ }
13450
+ const chunkUsage = chunk.usage;
13451
+ if (chunkUsage) {
13452
+ usage = {
13453
+ inputTokens: chunkUsage.prompt_tokens ?? 0,
13454
+ outputTokens: chunkUsage.completion_tokens ?? 0,
13455
+ totalTokens: chunkUsage.total_tokens ?? 0,
13456
+ cachedInputTokens: chunkUsage.prompt_tokens_details?.cached_tokens ?? void 0,
13457
+ reasoningTokens: chunkUsage.completion_tokens_details?.reasoning_tokens ?? void 0,
13458
+ searches: chunkUsage.num_search_queries ?? chunkUsage.server_tool_use?.web_search_requests ?? void 0,
13459
+ // Authoritative billed cost from OpenRouter usage accounting —
13460
+ // preferred over the catalog estimate (collector keeps it).
13461
+ costUSD: typeof chunkUsage.cost === "number" ? chunkUsage.cost : void 0
13462
+ };
13463
+ }
13464
+ }
13465
+ for (const { url, rawEvent } of bufferedLegacyCitations) {
13466
+ if (seenCitationUrls.has(url)) continue;
13467
+ seenCitationUrls.add(url);
13468
+ yield { type: "citation", citation: { url }, rawEvent };
13469
+ }
13470
+ if (usage) {
13471
+ yield { type: "usage", usage };
13472
+ }
13473
+ yield {
13474
+ type: "done",
13475
+ result: {
13476
+ status: terminalStatus ?? "completed",
13477
+ // Report text was streamed as deltas; the collector joins them.
13478
+ report: "",
13479
+ usage,
13480
+ raw: lastRaw
13481
+ }
13482
+ };
13483
+ }
13484
+ var init_openrouter_research = __esm({
13485
+ "src/providers/openrouter-research.ts"() {
13486
+ "use strict";
13487
+ init_errors2();
13488
+ }
13489
+ });
13490
+
13491
+ // src/providers/openrouter-research-models.ts
13492
+ function isOpenRouterResearchModel(modelId) {
13493
+ return byId3.has(modelId);
13494
+ }
13495
+ var OPENROUTER_STREAM_ONLY, openrouterResearchModels, byId3;
13496
+ var init_openrouter_research_models = __esm({
13497
+ "src/providers/openrouter-research-models.ts"() {
13498
+ "use strict";
13499
+ OPENROUTER_STREAM_ONLY = {
13500
+ streaming: true,
13501
+ background: false,
13502
+ resumable: false,
13503
+ tools: []
13504
+ };
13505
+ openrouterResearchModels = [
13506
+ {
13507
+ provider: "openrouter",
13508
+ modelId: "perplexity/sonar-deep-research",
13509
+ kind: "model",
13510
+ displayName: "Perplexity Sonar Deep Research",
13511
+ contextWindow: 128e3,
13512
+ pricing: {
13513
+ input: 2,
13514
+ output: 8,
13515
+ // Internal reasoning tokens are priced separately from output.
13516
+ internalReasoning: 3,
13517
+ perThousandSearches: 5
13518
+ },
13519
+ capabilities: OPENROUTER_STREAM_ONLY,
13520
+ metadata: {
13521
+ notes: "R1-style think-then-write: expect a long reasoning stream (100k+ tokens) before report text."
13522
+ }
13523
+ },
13524
+ {
13525
+ provider: "openrouter",
13526
+ modelId: "perplexity/sonar-pro-search",
13527
+ kind: "model",
13528
+ displayName: "Perplexity Sonar Pro Search",
13529
+ contextWindow: 2e5,
13530
+ pricing: { input: 3, output: 15, perThousandSearches: 18 },
13531
+ capabilities: OPENROUTER_STREAM_ONLY,
13532
+ metadata: { notes: "Agentic multi-step Pro Search \u2014 lighter than full deep research." }
13533
+ },
13534
+ {
13535
+ provider: "openrouter",
13536
+ modelId: "openai/o3-deep-research",
13537
+ kind: "model",
13538
+ displayName: "OpenAI o3 Deep Research (via OpenRouter)",
13539
+ contextWindow: 2e5,
13540
+ maxOutputTokens: 1e5,
13541
+ pricing: { input: 10, cachedInput: 2.5, output: 40, perThousandSearches: 10 },
13542
+ capabilities: OPENROUTER_STREAM_ONLY,
13543
+ metadata: {
13544
+ // Upstream OpenAI shutdown propagates through OpenRouter.
13545
+ deprecationDate: "2026-04-22",
13546
+ shutdownDate: "2026-07-23",
13547
+ replacement: "perplexity/sonar-deep-research",
13548
+ notes: "Tools are managed upstream \u2014 requests carry no tools param via OpenRouter."
13549
+ }
13550
+ },
13551
+ {
13552
+ provider: "openrouter",
13553
+ modelId: "openai/o4-mini-deep-research",
13554
+ kind: "model",
13555
+ displayName: "OpenAI o4-mini Deep Research (via OpenRouter)",
13556
+ contextWindow: 2e5,
13557
+ maxOutputTokens: 1e5,
13558
+ pricing: { input: 2, cachedInput: 0.5, output: 8, perThousandSearches: 10 },
13559
+ capabilities: OPENROUTER_STREAM_ONLY,
13560
+ metadata: {
13561
+ deprecationDate: "2026-04-22",
13562
+ shutdownDate: "2026-07-23",
13563
+ replacement: "perplexity/sonar-deep-research",
13564
+ notes: "Tools are managed upstream \u2014 requests carry no tools param via OpenRouter."
13565
+ }
13566
+ }
13567
+ ];
13568
+ byId3 = new Map(openrouterResearchModels.map((spec) => [spec.modelId, spec]));
13569
+ }
13570
+ });
13571
+
12118
13572
  // src/providers/openrouter-speech-models.ts
12119
13573
  function getOpenRouterSpeechModelSpec(modelId) {
12120
13574
  return openrouterSpeechModels.find((m) => m.modelId === modelId);
@@ -12242,8 +13696,11 @@ var init_openrouter = __esm({
12242
13696
  import_openai4 = __toESM(require("openai"), 1);
12243
13697
  init_model_shortcuts();
12244
13698
  init_logger();
13699
+ init_constants3();
12245
13700
  init_openai_compatible_provider();
12246
13701
  init_openrouter_models();
13702
+ init_openrouter_research();
13703
+ init_openrouter_research_models();
12247
13704
  init_openrouter_speech_models();
12248
13705
  init_utils();
12249
13706
  logger = createLogger({ name: "openrouter" });
@@ -12263,12 +13720,72 @@ var init_openrouter = __esm({
12263
13720
  getModelSpecs() {
12264
13721
  return OPENROUTER_MODELS;
12265
13722
  }
12266
- /**
12267
- * Override buildApiRequest to inject reasoning parameters and cache_control breakpoints.
12268
- * OpenRouter normalizes reasoning into the standard OpenAI format,
12269
- * and supports cache_control on message content blocks for both
12270
- * Anthropic Claude and Google Gemini models.
12271
- */
13723
+ // =========================================================================
13724
+ // Deep Research (chat-completions surface)
13725
+ //
13726
+ // Stream-only: no background mode, no job id, no resume. resumeResearch /
13727
+ // getResearchStatus / cancelResearch are deliberately NOT implemented — the
13728
+ // research job surfaces typed errors for those operations, and a dropped
13729
+ // stream is never silently re-run (a multi-dollar research run).
13730
+ // =========================================================================
13731
+ getResearchModelSpecs() {
13732
+ return openrouterResearchModels;
13733
+ }
13734
+ supportsResearch(modelId) {
13735
+ return isOpenRouterResearchModel(modelId);
13736
+ }
13737
+ startResearch(options, descriptor, _spec) {
13738
+ const { messages } = buildOpenRouterResearchMessages(options);
13739
+ const client = this.client;
13740
+ const request = {
13741
+ model: descriptor.name,
13742
+ messages,
13743
+ // Mandatory: multi-minute runs hit idle disconnects without streaming.
13744
+ stream: true,
13745
+ stream_options: { include_usage: true },
13746
+ // OpenRouter usage accounting: report the authoritative billed cost
13747
+ // (covers per-search fees that token-based estimation cannot see).
13748
+ usage: { include: true }
13749
+ };
13750
+ if (options.reasoning?.enabled !== void 0) {
13751
+ request.reasoning = {
13752
+ effort: OPENROUTER_EFFORT_MAP[options.reasoning.effort ?? "medium"]
13753
+ };
13754
+ }
13755
+ const protectedKeys = /* @__PURE__ */ new Set(["model", "messages", "stream", "stream_options", "usage"]);
13756
+ Object.assign(request, this.buildProviderSpecificParams(options.extra));
13757
+ for (const [key, value] of Object.entries(options.extra ?? {})) {
13758
+ if (!this.isProviderSpecificKey(key) && !protectedKeys.has(key)) {
13759
+ request[key] = value;
13760
+ }
13761
+ }
13762
+ const headers = this.getCustomHeaders();
13763
+ const requestOptions3 = {
13764
+ signal: options.signal,
13765
+ // Research runs take minutes — override the client's default timeout.
13766
+ timeout: OPENROUTER_RESEARCH_HTTP_TIMEOUT_MS,
13767
+ ...Object.keys(headers).length > 0 ? { headers } : {}
13768
+ };
13769
+ const enhance = (error) => this.enhanceError(error);
13770
+ return (async function* () {
13771
+ let stream2;
13772
+ try {
13773
+ stream2 = await client.chat.completions.create(
13774
+ request,
13775
+ requestOptions3
13776
+ );
13777
+ } catch (error) {
13778
+ throw enhance(error);
13779
+ }
13780
+ yield* normalizeOpenRouterResearchStream(stream2);
13781
+ })();
13782
+ }
13783
+ /**
13784
+ * Override buildApiRequest to inject reasoning parameters and cache_control breakpoints.
13785
+ * OpenRouter normalizes reasoning into the standard OpenAI format,
13786
+ * and supports cache_control on message content blocks for both
13787
+ * Anthropic Claude and Google Gemini models.
13788
+ */
12272
13789
  buildApiRequest(options, descriptor, spec, messages) {
12273
13790
  const request = super.buildApiRequest(options, descriptor, spec, messages);
12274
13791
  if (options.reasoning?.enabled !== void 0) {
@@ -12381,150 +13898,764 @@ Original error: ${error.message}`
12381
13898
  Original error: ${error.message}`
12382
13899
  );
12383
13900
  }
12384
- if (message.includes("503") || message.includes("unavailable")) {
12385
- return new Error(
12386
- `OpenRouter: Model temporarily unavailable. Try a different model or use the 'models' fallback option for automatic retry.
12387
- Original error: ${error.message}`
13901
+ if (message.includes("503") || message.includes("unavailable")) {
13902
+ return new Error(
13903
+ `OpenRouter: Model temporarily unavailable. Try a different model or use the 'models' fallback option for automatic retry.
13904
+ Original error: ${error.message}`
13905
+ );
13906
+ }
13907
+ if (message.includes("400") || message.includes("bad request")) {
13908
+ const enhanced = new Error(
13909
+ `OpenRouter: Provider returned error (400). This may indicate the request exceeded the model's limits, or a model-specific rejection.
13910
+ Original error: ${error.message}`
13911
+ );
13912
+ const originalStatus = error.status;
13913
+ Object.assign(enhanced, {
13914
+ status: typeof originalStatus === "number" ? originalStatus : 400
13915
+ });
13916
+ return enhanced;
13917
+ }
13918
+ if (message.includes("401") || message.includes("unauthorized") || message.includes("invalid api key") || message.includes("invalid key") || message.includes("invalid_api_key")) {
13919
+ return new Error(
13920
+ `OpenRouter: Authentication failed. Check that OPENROUTER_API_KEY is set correctly.
13921
+ Original error: ${error.message}`
13922
+ );
13923
+ }
13924
+ return error;
13925
+ }
13926
+ // =========================================================================
13927
+ // Speech Generation (TTS via Chat Completions with Audio Modality)
13928
+ // =========================================================================
13929
+ /**
13930
+ * Get speech model specifications for OpenRouter.
13931
+ */
13932
+ getSpeechModelSpecs() {
13933
+ return openrouterSpeechModels;
13934
+ }
13935
+ /**
13936
+ * Check if this provider supports speech generation for a given model.
13937
+ * Handles both prefixed (openrouter:openai/gpt-audio-mini) and unprefixed model IDs.
13938
+ */
13939
+ supportsSpeechGeneration(modelId) {
13940
+ const bareModelId = getModelId(modelId);
13941
+ return isOpenRouterSpeechModel(bareModelId);
13942
+ }
13943
+ /**
13944
+ * Generate speech audio from text using OpenRouter's audio-capable models.
13945
+ *
13946
+ * OpenRouter TTS works via chat completions with audio modality, not a
13947
+ * dedicated TTS endpoint. The model receives a prompt asking it to say
13948
+ * the text, and returns audio data via streaming.
13949
+ *
13950
+ * @param options - Speech generation options
13951
+ * @returns Promise resolving to the generation result with audio and cost
13952
+ * @throws Error if model is unknown, voice/format are invalid, or no audio is returned
13953
+ */
13954
+ async generateSpeech(options) {
13955
+ const client = this.client;
13956
+ const bareModelId = getModelId(options.model);
13957
+ const spec = getOpenRouterSpeechModelSpec(bareModelId);
13958
+ if (!spec) {
13959
+ throw new Error(`Unknown OpenRouter TTS model: ${options.model}`);
13960
+ }
13961
+ const voice = options.voice ?? spec.defaultVoice ?? "alloy";
13962
+ if (!spec.voices.includes(voice)) {
13963
+ throw new Error(
13964
+ `Invalid voice "${voice}" for ${options.model}. Valid voices: ${spec.voices.join(", ")}`
13965
+ );
13966
+ }
13967
+ const format2 = options.responseFormat ?? spec.defaultFormat ?? "mp3";
13968
+ if (!spec.formats.includes(format2)) {
13969
+ throw new Error(
13970
+ `Invalid format "${format2}" for ${options.model}. Valid formats: ${spec.formats.join(", ")}`
13971
+ );
13972
+ }
13973
+ logger.debug("TTS request", {
13974
+ model: bareModelId,
13975
+ voice,
13976
+ format: format2,
13977
+ inputLength: options.input.length
13978
+ });
13979
+ try {
13980
+ const response = await client.chat.completions.create({
13981
+ model: bareModelId,
13982
+ messages: [
13983
+ {
13984
+ role: "user",
13985
+ content: `Please say the following text exactly: "${options.input}"`
13986
+ }
13987
+ ],
13988
+ // OpenRouter-specific parameters for audio output
13989
+ // Note: OpenRouter TTS via chat completions does NOT support the speed parameter
13990
+ // (unlike OpenAI's dedicated /audio/speech endpoint which does)
13991
+ modalities: ["text", "audio"],
13992
+ audio: {
13993
+ voice,
13994
+ format: format2
13995
+ },
13996
+ stream: true
13997
+ });
13998
+ const audioChunks = [];
13999
+ for await (const chunk of response) {
14000
+ const delta = chunk.choices[0]?.delta;
14001
+ if (!delta || typeof delta !== "object") continue;
14002
+ const audioObj = delta.audio;
14003
+ if (!audioObj || typeof audioObj !== "object") continue;
14004
+ const audioData = audioObj.data;
14005
+ if (typeof audioData !== "string" || audioData.length === 0) continue;
14006
+ const decoded = Buffer.from(audioData, "base64");
14007
+ if (decoded.length === 0) {
14008
+ throw new Error("Invalid base64 audio data received from OpenRouter");
14009
+ }
14010
+ audioChunks.push(decoded);
14011
+ }
14012
+ const audioBuffer = Buffer.concat(audioChunks);
14013
+ if (audioBuffer.length === 0) {
14014
+ throw new Error(
14015
+ "OpenRouter TTS returned no audio data. The model may have failed to generate audio or the stream was interrupted."
14016
+ );
14017
+ }
14018
+ const cost = calculateOpenRouterSpeechCost(bareModelId, options.input.length);
14019
+ return {
14020
+ // Use Uint8Array for clean ArrayBuffer extraction (safer than buffer.slice for Node.js Buffer)
14021
+ audio: new Uint8Array(audioBuffer).buffer,
14022
+ model: options.model,
14023
+ usage: {
14024
+ characterCount: options.input.length
14025
+ },
14026
+ cost,
14027
+ format: format2
14028
+ };
14029
+ } catch (error) {
14030
+ const err = error;
14031
+ const apiError = err.error?.message || err.error?.code || "";
14032
+ const bodyInfo = err.body ? JSON.stringify(err.body) : "";
14033
+ const details = apiError || bodyInfo || err.message || "Unknown error";
14034
+ logger.debug("TTS error", {
14035
+ model: bareModelId,
14036
+ voice,
14037
+ format: format2,
14038
+ status: err.status,
14039
+ error: err.error,
14040
+ body: err.body,
14041
+ message: err.message
14042
+ });
14043
+ throw new Error(
14044
+ `OpenRouter TTS failed for model ${bareModelId}: ${details}` + (err.status ? ` (HTTP ${err.status})` : "")
14045
+ );
14046
+ }
14047
+ }
14048
+ };
14049
+ }
14050
+ });
14051
+
14052
+ // src/providers/discovery.ts
14053
+ function discoverProviderAdapters() {
14054
+ const adapters = [];
14055
+ for (const discover of DISCOVERERS) {
14056
+ const adapter = discover();
14057
+ if (adapter) {
14058
+ adapters.push(adapter);
14059
+ }
14060
+ }
14061
+ return adapters;
14062
+ }
14063
+ var DISCOVERERS;
14064
+ var init_discovery = __esm({
14065
+ "src/providers/discovery.ts"() {
14066
+ "use strict";
14067
+ init_anthropic();
14068
+ init_gemini();
14069
+ init_huggingface();
14070
+ init_openai();
14071
+ init_openrouter();
14072
+ DISCOVERERS = [
14073
+ createOpenAIProviderFromEnv,
14074
+ createAnthropicProviderFromEnv,
14075
+ createGeminiProviderFromEnv,
14076
+ createHuggingFaceProviderFromEnv,
14077
+ createOpenRouterProviderFromEnv
14078
+ ];
14079
+ }
14080
+ });
14081
+
14082
+ // src/research/cost.ts
14083
+ function estimateResearchCost(pricing, usage) {
14084
+ const cachedTokens = usage.cachedInputTokens ?? 0;
14085
+ const freshInputTokens = Math.max(0, usage.inputTokens - cachedTokens);
14086
+ const cachedRate = pricing.cachedInput ?? pricing.input;
14087
+ let outputTokens = usage.outputTokens;
14088
+ let reasoningCost = 0;
14089
+ if (pricing.internalReasoning !== void 0 && usage.reasoningTokens !== void 0) {
14090
+ outputTokens = Math.max(0, outputTokens - usage.reasoningTokens);
14091
+ reasoningCost = usage.reasoningTokens * pricing.internalReasoning / TOKENS_PER_MILLION;
14092
+ }
14093
+ const searchCost = pricing.perThousandSearches !== void 0 && usage.searches !== void 0 ? usage.searches * pricing.perThousandSearches / SEARCHES_PER_THOUSAND : 0;
14094
+ const total = freshInputTokens * pricing.input / TOKENS_PER_MILLION + cachedTokens * cachedRate / TOKENS_PER_MILLION + outputTokens * pricing.output / TOKENS_PER_MILLION + reasoningCost + searchCost;
14095
+ return Number(total.toFixed(RESEARCH_COST_DECIMALS));
14096
+ }
14097
+ var init_cost = __esm({
14098
+ "src/research/cost.ts"() {
14099
+ "use strict";
14100
+ init_constants3();
14101
+ }
14102
+ });
14103
+
14104
+ // src/research/collector.ts
14105
+ function citationKey2(citation) {
14106
+ return `${citation.url}#${citation.startIndex ?? ""}`;
14107
+ }
14108
+ function max(a, b) {
14109
+ if (a === void 0) return b;
14110
+ if (b === void 0) return a;
14111
+ return Math.max(a, b);
14112
+ }
14113
+ var EMPTY_USAGE, TERMINAL_STATUSES, ResearchResultCollector;
14114
+ var init_collector = __esm({
14115
+ "src/research/collector.ts"() {
14116
+ "use strict";
14117
+ init_cost();
14118
+ EMPTY_USAGE = { inputTokens: 0, outputTokens: 0, totalTokens: 0 };
14119
+ TERMINAL_STATUSES = /* @__PURE__ */ new Set([
14120
+ "completed",
14121
+ "failed",
14122
+ "cancelled",
14123
+ "incomplete",
14124
+ "budget_exceeded"
14125
+ ]);
14126
+ ResearchResultCollector = class {
14127
+ constructor(spec, now = Date.now) {
14128
+ this.spec = spec;
14129
+ this.now = now;
14130
+ }
14131
+ spec;
14132
+ now;
14133
+ reportParts = [];
14134
+ citations = /* @__PURE__ */ new Map();
14135
+ usage;
14136
+ lastStatus;
14137
+ doneReport = "";
14138
+ doneRaw;
14139
+ hasDone = false;
14140
+ firstEventAt;
14141
+ terminalAt;
14142
+ /**
14143
+ * Terminal error info from an `error` event.
14144
+ * @internal Read by tests only — not part of the public consumer contract.
14145
+ */
14146
+ terminalError;
14147
+ ingest(event) {
14148
+ this.firstEventAt ??= this.now();
14149
+ switch (event.type) {
14150
+ case "text":
14151
+ this.reportParts.push(event.delta);
14152
+ break;
14153
+ case "citation":
14154
+ this.addCitation(event.citation);
14155
+ break;
14156
+ case "usage":
14157
+ this.mergeUsage(event.usage);
14158
+ break;
14159
+ case "status":
14160
+ this.lastStatus = event.status;
14161
+ break;
14162
+ case "error":
14163
+ this.terminalError = event.error;
14164
+ this.terminalAt = this.now();
14165
+ break;
14166
+ case "done": {
14167
+ this.hasDone = true;
14168
+ this.lastStatus = event.result.status;
14169
+ this.doneReport = event.result.report;
14170
+ this.doneRaw = event.result.raw;
14171
+ for (const citation of event.result.citations ?? []) {
14172
+ this.addCitation(citation);
14173
+ }
14174
+ if (event.result.usage) {
14175
+ this.mergeUsage(event.result.usage);
14176
+ }
14177
+ this.terminalAt = this.now();
14178
+ break;
14179
+ }
14180
+ default:
14181
+ break;
14182
+ }
14183
+ }
14184
+ toResult(context) {
14185
+ const usage = { ...this.usage ?? EMPTY_USAGE };
14186
+ if (usage.costUSD === void 0 && this.spec) {
14187
+ usage.costUSD = estimateResearchCost(this.spec.pricing, usage);
14188
+ }
14189
+ const status = this.lastStatus ?? (this.terminalError ? "failed" : "in_progress");
14190
+ const lastIsTerminal = this.lastStatus !== void 0 && TERMINAL_STATUSES.has(this.lastStatus);
14191
+ return {
14192
+ jobId: context.jobId,
14193
+ provider: context.provider,
14194
+ model: context.model,
14195
+ // An error fails the run only when no explicit terminal status preceded
14196
+ // it (a timeout's "incomplete" is a partial, not a failure).
14197
+ status: this.terminalError && !this.hasDone && !lastIsTerminal ? "failed" : status,
14198
+ report: this.doneReport !== "" ? this.doneReport : this.reportParts.join(""),
14199
+ citations: [...this.citations.values()],
14200
+ usage,
14201
+ durationMs: this.firstEventAt !== void 0 && this.terminalAt !== void 0 ? this.terminalAt - this.firstEventAt : void 0,
14202
+ raw: this.doneRaw
14203
+ };
14204
+ }
14205
+ /** Whether a terminal event (`done` or `error`) was ingested. */
14206
+ get isTerminal() {
14207
+ return this.hasDone || this.terminalError !== void 0;
14208
+ }
14209
+ addCitation(citation) {
14210
+ const key = citationKey2(citation);
14211
+ if (!this.citations.has(key)) {
14212
+ this.citations.set(key, citation);
14213
+ }
14214
+ }
14215
+ mergeUsage(incoming) {
14216
+ const current = this.usage ?? { ...EMPTY_USAGE };
14217
+ this.usage = {
14218
+ ...current,
14219
+ ...incoming,
14220
+ // Cumulative counters may be re-reported; never let them regress.
14221
+ searches: max(current.searches, incoming.searches)
14222
+ };
14223
+ }
14224
+ };
14225
+ }
14226
+ });
14227
+
14228
+ // src/research/job.ts
14229
+ var ResearchJobImpl;
14230
+ var init_job = __esm({
14231
+ "src/research/job.ts"() {
14232
+ "use strict";
14233
+ init_errors();
14234
+ init_collector();
14235
+ init_constants3();
14236
+ init_errors2();
14237
+ ResearchJobImpl = class {
14238
+ adapter;
14239
+ descriptor;
14240
+ spec;
14241
+ options;
14242
+ resumeFrom;
14243
+ collector;
14244
+ controller = new AbortController();
14245
+ timeoutMs;
14246
+ state = "idle";
14247
+ currentJobId;
14248
+ cursor;
14249
+ startedAt;
14250
+ timedOut = false;
14251
+ cancelRequested = false;
14252
+ finalResult;
14253
+ failure;
14254
+ completion;
14255
+ constructor(init) {
14256
+ this.adapter = init.adapter;
14257
+ this.descriptor = init.descriptor;
14258
+ this.spec = init.spec;
14259
+ this.options = init.options;
14260
+ this.resumeFrom = init.resumeFrom;
14261
+ this.collector = new ResearchResultCollector(init.spec);
14262
+ this.currentJobId = init.resumeFrom?.jobId ?? null;
14263
+ this.cursor = init.resumeFrom?.cursor;
14264
+ this.startedAt = init.resumeFrom?.startedAt;
14265
+ const specCap = init.spec?.maxDurationMs ?? Number.POSITIVE_INFINITY;
14266
+ this.timeoutMs = init.options?.timeoutMs ?? Math.min(RESEARCH_DEFAULT_TIMEOUT_MS, specCap);
14267
+ if (init.options?.signal) {
14268
+ const external = init.options.signal;
14269
+ if (external.aborted) {
14270
+ this.controller.abort(external.reason);
14271
+ } else {
14272
+ external.addEventListener("abort", () => this.controller.abort(external.reason), {
14273
+ once: true
14274
+ });
14275
+ }
14276
+ }
14277
+ }
14278
+ get jobId() {
14279
+ return this.currentJobId;
14280
+ }
14281
+ get provider() {
14282
+ return this.adapter.providerId;
14283
+ }
14284
+ get model() {
14285
+ return this.descriptor.name;
14286
+ }
14287
+ [Symbol.asyncIterator]() {
14288
+ return this.events()[Symbol.asyncIterator]();
14289
+ }
14290
+ events() {
14291
+ if (this.state !== "idle") {
14292
+ throw new ResearchStreamConsumedError();
14293
+ }
14294
+ this.state = "streaming";
14295
+ let resolveCompletion = () => {
14296
+ };
14297
+ const promise = new Promise((resolve2) => {
14298
+ resolveCompletion = resolve2;
14299
+ });
14300
+ this.completion = { promise, resolve: resolveCompletion };
14301
+ return this.run();
14302
+ }
14303
+ async result() {
14304
+ if (this.state === "idle") {
14305
+ for await (const _event of this.events()) {
14306
+ }
14307
+ } else if (this.state === "streaming" && this.completion) {
14308
+ await this.completion.promise;
14309
+ }
14310
+ if (this.failure) {
14311
+ throw this.failure;
14312
+ }
14313
+ this.finalResult ??= this.collector.toResult(this.resultContext());
14314
+ return this.finalResult;
14315
+ }
14316
+ async status() {
14317
+ if (!this.adapter.getResearchStatus) {
14318
+ throw new ResearchNotPollableError(
14319
+ `Provider "${this.provider}" does not support research status polling.`
14320
+ );
14321
+ }
14322
+ if (this.currentJobId === null) {
14323
+ throw new ResearchNotPollableError(
14324
+ `Research run on "${this.provider}" has no server-side job id to poll.`
14325
+ );
14326
+ }
14327
+ const snapshot = await this.adapter.getResearchStatus(this.buildRef());
14328
+ return snapshot.status;
14329
+ }
14330
+ async cancel() {
14331
+ this.cancelRequested = true;
14332
+ if (this.adapter.cancelResearch && this.currentJobId !== null) {
14333
+ await this.adapter.cancelResearch(this.buildRef());
14334
+ }
14335
+ this.controller.abort();
14336
+ }
14337
+ toRef() {
14338
+ if (this.currentJobId === null) {
14339
+ throw new ResearchJobNotResumableError(
14340
+ `Research run on "${this.provider}" has no server-side job id \u2014 the provider does not support background jobs, so it cannot be re-attached.`
14341
+ );
14342
+ }
14343
+ return this.buildRef();
14344
+ }
14345
+ buildRef() {
14346
+ if (this.currentJobId === null) {
14347
+ throw new ResearchJobNotResumableError("Research job has no server-side job id.");
14348
+ }
14349
+ return {
14350
+ provider: this.provider,
14351
+ model: this.descriptor.name,
14352
+ jobId: this.currentJobId,
14353
+ cursor: this.cursor,
14354
+ startedAt: this.startedAt
14355
+ };
14356
+ }
14357
+ resultContext() {
14358
+ return { provider: this.provider, model: this.descriptor.name, jobId: this.currentJobId };
14359
+ }
14360
+ openStream(resume) {
14361
+ if (resume) {
14362
+ if (!this.adapter.resumeResearch) {
14363
+ throw new ResearchJobNotResumableError(
14364
+ `Provider "${this.provider}" does not support resuming research jobs.`
14365
+ );
14366
+ }
14367
+ return this.adapter.resumeResearch(this.buildRef(), this.controller.signal);
14368
+ }
14369
+ if (!this.adapter.startResearch || !this.options) {
14370
+ throw new ResearchJobNotResumableError(
14371
+ `Provider "${this.provider}" cannot start this research job.`
14372
+ );
14373
+ }
14374
+ return this.adapter.startResearch(
14375
+ { ...this.options, signal: this.controller.signal },
14376
+ this.descriptor,
14377
+ this.spec
14378
+ );
14379
+ }
14380
+ observe(event) {
14381
+ if (event.cursor !== void 0) {
14382
+ this.cursor = event.cursor;
14383
+ }
14384
+ if (event.type === "created") {
14385
+ if (event.jobId !== null) {
14386
+ this.currentJobId = event.jobId;
14387
+ }
14388
+ this.startedAt ??= (/* @__PURE__ */ new Date()).toISOString();
14389
+ }
14390
+ this.collector.ingest(event);
14391
+ }
14392
+ canResume() {
14393
+ if (this.adapter.resumeResearch === void 0 || this.currentJobId === null) {
14394
+ return false;
14395
+ }
14396
+ return this.spec ? this.spec.capabilities.resumable : true;
14397
+ }
14398
+ async *run() {
14399
+ const timer = setTimeout(() => {
14400
+ this.timedOut = true;
14401
+ this.controller.abort(new ResearchTimeoutError(this.timeoutMs));
14402
+ }, this.timeoutMs);
14403
+ timer.unref?.();
14404
+ let reconnectAttempts = 0;
14405
+ let source = this.openStream(this.resumeFrom !== void 0);
14406
+ try {
14407
+ while (true) {
14408
+ const iterator = source[Symbol.asyncIterator]();
14409
+ try {
14410
+ while (true) {
14411
+ const next = await iterator.next();
14412
+ if (next.done) {
14413
+ return;
14414
+ }
14415
+ reconnectAttempts = 0;
14416
+ this.observe(next.value);
14417
+ yield next.value;
14418
+ if (this.collector.isTerminal) {
14419
+ return;
14420
+ }
14421
+ }
14422
+ } catch (error) {
14423
+ const err = error instanceof Error ? error : new Error(String(error));
14424
+ if (this.timedOut) {
14425
+ const statusEvent = { type: "status", status: "incomplete" };
14426
+ this.collector.ingest(statusEvent);
14427
+ yield statusEvent;
14428
+ const info2 = {
14429
+ message: new ResearchTimeoutError(this.timeoutMs).message,
14430
+ code: "timeout",
14431
+ retryable: false
14432
+ };
14433
+ this.collector.ingest({ type: "error", error: info2 });
14434
+ yield { type: "error", error: info2 };
14435
+ return;
14436
+ }
14437
+ if (this.cancelRequested && isAbortError(err)) {
14438
+ const event = { type: "status", status: "cancelled" };
14439
+ this.observe(event);
14440
+ yield event;
14441
+ return;
14442
+ }
14443
+ if (isAbortError(err)) {
14444
+ this.failure = err;
14445
+ throw err;
14446
+ }
14447
+ if (this.canResume() && reconnectAttempts < RESEARCH_STREAM_RECONNECT_MAX_ATTEMPTS) {
14448
+ reconnectAttempts += 1;
14449
+ source = this.openStream(true);
14450
+ continue;
14451
+ }
14452
+ const info = { message: err.message, retryable: false };
14453
+ this.collector.ingest({ type: "error", error: info });
14454
+ yield { type: "error", error: info };
14455
+ return;
14456
+ }
14457
+ }
14458
+ } finally {
14459
+ clearTimeout(timer);
14460
+ this.state = "finished";
14461
+ this.finalResult = this.collector.toResult(this.resultContext());
14462
+ this.completion?.resolve();
14463
+ }
14464
+ }
14465
+ };
14466
+ }
14467
+ });
14468
+
14469
+ // src/research/types.ts
14470
+ var RESEARCH_DATA_SOURCE_TOOL_TYPES;
14471
+ var init_types = __esm({
14472
+ "src/research/types.ts"() {
14473
+ "use strict";
14474
+ RESEARCH_DATA_SOURCE_TOOL_TYPES = [
14475
+ "web_search",
14476
+ "file_search",
14477
+ "mcp"
14478
+ ];
14479
+ }
14480
+ });
14481
+
14482
+ // src/research/namespace.ts
14483
+ var ResearchNamespace;
14484
+ var init_namespace = __esm({
14485
+ "src/research/namespace.ts"() {
14486
+ "use strict";
14487
+ init_logger();
14488
+ init_constants3();
14489
+ init_errors2();
14490
+ init_job();
14491
+ init_types();
14492
+ ResearchNamespace = class {
14493
+ constructor(adapters, parser, now = Date.now, logger2) {
14494
+ this.adapters = adapters;
14495
+ this.parser = parser;
14496
+ this.now = now;
14497
+ this.logger = logger2 ?? createLogger({ name: "llmist:research" });
14498
+ }
14499
+ adapters;
14500
+ parser;
14501
+ now;
14502
+ logger;
14503
+ /**
14504
+ * Start a research run. Returns immediately; the provider stream opens
14505
+ * lazily on first iteration (or on `result()`).
14506
+ */
14507
+ start(options) {
14508
+ const descriptor = this.parser.parse(options.model);
14509
+ const adapter = this.findResearchAdapter(descriptor);
14510
+ if (!adapter) {
14511
+ throw new ResearchNotSupportedError(
14512
+ `No provider supports deep research for model "${options.model}". Research-capable models: ${this.describeAvailableModels()}`
14513
+ );
14514
+ }
14515
+ const spec = this.findSpec(adapter, descriptor.name);
14516
+ const validated = spec ? this.validate(options, spec) : options;
14517
+ return new ResearchJobImpl({ adapter, descriptor, spec, options: validated });
14518
+ }
14519
+ /**
14520
+ * Re-attach to a background research job from a serialized ref.
14521
+ * No network happens until the returned job is iterated.
14522
+ */
14523
+ attach(ref) {
14524
+ const adapter = this.findAdapterByProviderId(ref.provider);
14525
+ if (!adapter) {
14526
+ throw new ResearchNotSupportedError(
14527
+ `No registered provider with id "${ref.provider}" to attach research job "${ref.jobId}".`
14528
+ );
14529
+ }
14530
+ if (!adapter.resumeResearch) {
14531
+ throw new ResearchJobNotResumableError(
14532
+ `Provider "${ref.provider}" does not support resuming research jobs.`
12388
14533
  );
12389
14534
  }
12390
- if (message.includes("400") || message.includes("bad request")) {
12391
- const enhanced = new Error(
12392
- `OpenRouter: Provider returned error (400). This may indicate the request exceeded the model's limits, or a model-specific rejection.
12393
- Original error: ${error.message}`
14535
+ const descriptor = { provider: ref.provider, name: ref.model };
14536
+ const spec = this.findSpec(adapter, ref.model);
14537
+ return new ResearchJobImpl({ adapter, descriptor, spec, resumeFrom: ref });
14538
+ }
14539
+ /** One-shot status poll for a job ref. */
14540
+ async get(ref) {
14541
+ const adapter = this.findAdapterByProviderId(ref.provider);
14542
+ if (!adapter) {
14543
+ throw new ResearchNotSupportedError(`No registered provider with id "${ref.provider}".`);
14544
+ }
14545
+ if (!adapter.getResearchStatus) {
14546
+ throw new ResearchNotPollableError(
14547
+ `Provider "${ref.provider}" does not support research status polling.`
12394
14548
  );
12395
- const originalStatus = error.status;
12396
- Object.assign(enhanced, {
12397
- status: typeof originalStatus === "number" ? originalStatus : 400
12398
- });
12399
- return enhanced;
12400
14549
  }
12401
- if (message.includes("401") || message.includes("unauthorized") || message.includes("invalid api key") || message.includes("invalid key") || message.includes("invalid_api_key")) {
12402
- return new Error(
12403
- `OpenRouter: Authentication failed. Check that OPENROUTER_API_KEY is set correctly.
12404
- Original error: ${error.message}`
14550
+ return adapter.getResearchStatus(ref);
14551
+ }
14552
+ /** Cancel a background job server-side. */
14553
+ async cancel(ref) {
14554
+ const adapter = this.findAdapterByProviderId(ref.provider);
14555
+ if (!adapter) {
14556
+ throw new ResearchNotSupportedError(`No registered provider with id "${ref.provider}".`);
14557
+ }
14558
+ if (!adapter.cancelResearch) {
14559
+ throw new ResearchNotSupportedError(
14560
+ `Provider "${ref.provider}" does not support cancelling research jobs.`
12405
14561
  );
12406
14562
  }
12407
- return error;
14563
+ return adapter.cancelResearch(ref);
12408
14564
  }
12409
- // =========================================================================
12410
- // Speech Generation (TTS via Chat Completions with Audio Modality)
12411
- // =========================================================================
12412
- /**
12413
- * Get speech model specifications for OpenRouter.
12414
- */
12415
- getSpeechModelSpecs() {
12416
- return openrouterSpeechModels;
14565
+ /** All research-capable models/agents across registered providers. */
14566
+ listModels() {
14567
+ const specs = [];
14568
+ for (const adapter of this.adapters) {
14569
+ specs.push(...adapter.getResearchModelSpecs?.() ?? []);
14570
+ }
14571
+ return specs;
12417
14572
  }
12418
- /**
12419
- * Check if this provider supports speech generation for a given model.
12420
- * Handles both prefixed (openrouter:openai/gpt-audio-mini) and unprefixed model IDs.
12421
- */
12422
- supportsSpeechGeneration(modelId) {
12423
- const bareModelId = getModelId(modelId);
12424
- return isOpenRouterSpeechModel(bareModelId);
14573
+ /** Whether any registered provider supports research for this model. */
14574
+ supportsModel(model) {
14575
+ try {
14576
+ return this.findResearchAdapter(this.parser.parse(model)) !== void 0;
14577
+ } catch {
14578
+ return false;
14579
+ }
14580
+ }
14581
+ findResearchAdapter(descriptor) {
14582
+ return this.adapters.find(
14583
+ (adapter) => adapter.supports(descriptor) && (adapter.supportsResearch?.(descriptor.name) ?? false)
14584
+ );
14585
+ }
14586
+ findAdapterByProviderId(providerId) {
14587
+ return this.adapters.find((adapter) => adapter.providerId === providerId);
14588
+ }
14589
+ findSpec(adapter, modelId) {
14590
+ return adapter.getResearchModelSpecs?.().find((spec) => spec.modelId === modelId);
14591
+ }
14592
+ describeAvailableModels() {
14593
+ const models = this.listModels();
14594
+ if (models.length === 0) {
14595
+ return "(none registered)";
14596
+ }
14597
+ return models.map((spec) => `${spec.provider}:${spec.modelId}`).join(", ");
12425
14598
  }
12426
14599
  /**
12427
- * Generate speech audio from text using OpenRouter's audio-capable models.
12428
- *
12429
- * OpenRouter TTS works via chat completions with audio modality, not a
12430
- * dedicated TTS endpoint. The model receives a prompt asking it to say
12431
- * the text, and returns audio data via streaming.
12432
- *
12433
- * @param options - Speech generation options
12434
- * @returns Promise resolving to the generation result with audio and cost
12435
- * @throws Error if model is unknown, voice/format are invalid, or no audio is returned
14600
+ * Pre-flight validation against the catalog spec fails fast before any
14601
+ * network call and applies spec-driven defaults.
12436
14602
  */
12437
- async generateSpeech(options) {
12438
- const client = this.client;
12439
- const bareModelId = getModelId(options.model);
12440
- const spec = getOpenRouterSpeechModelSpec(bareModelId);
12441
- if (!spec) {
12442
- throw new Error(`Unknown OpenRouter TTS model: ${options.model}`);
12443
- }
12444
- const voice = options.voice ?? spec.defaultVoice ?? "alloy";
12445
- if (!spec.voices.includes(voice)) {
12446
- throw new Error(
12447
- `Invalid voice "${voice}" for ${options.model}. Valid voices: ${spec.voices.join(", ")}`
14603
+ validate(options, spec) {
14604
+ this.enforceLifecycle(spec);
14605
+ const background = options.background ?? spec.capabilities.background;
14606
+ if (background && !spec.capabilities.background) {
14607
+ throw new ResearchValidationError(
14608
+ `Model "${spec.modelId}" does not support background research jobs.`
12448
14609
  );
12449
14610
  }
12450
- const format2 = options.responseFormat ?? spec.defaultFormat ?? "mp3";
12451
- if (!spec.formats.includes(format2)) {
12452
- throw new Error(
12453
- `Invalid format "${format2}" for ${options.model}. Valid formats: ${spec.formats.join(", ")}`
14611
+ if (options.previousJobId && !spec.capabilities.followUps) {
14612
+ throw new ResearchValidationError(
14613
+ `Model "${spec.modelId}" does not support follow-up research runs (previousJobId).`
12454
14614
  );
12455
14615
  }
12456
- logger.debug("TTS request", {
12457
- model: bareModelId,
12458
- voice,
12459
- format: format2,
12460
- inputLength: options.input.length
12461
- });
12462
- try {
12463
- const response = await client.chat.completions.create({
12464
- model: bareModelId,
12465
- messages: [
12466
- {
12467
- role: "user",
12468
- content: `Please say the following text exactly: "${options.input}"`
12469
- }
12470
- ],
12471
- // OpenRouter-specific parameters for audio output
12472
- // Note: OpenRouter TTS via chat completions does NOT support the speed parameter
12473
- // (unlike OpenAI's dedicated /audio/speech endpoint which does)
12474
- modalities: ["text", "audio"],
12475
- audio: {
12476
- voice,
12477
- format: format2
12478
- },
12479
- stream: true
12480
- });
12481
- const audioChunks = [];
12482
- for await (const chunk of response) {
12483
- const delta = chunk.choices[0]?.delta;
12484
- if (!delta || typeof delta !== "object") continue;
12485
- const audioObj = delta.audio;
12486
- if (!audioObj || typeof audioObj !== "object") continue;
12487
- const audioData = audioObj.data;
12488
- if (typeof audioData !== "string" || audioData.length === 0) continue;
12489
- const decoded = Buffer.from(audioData, "base64");
12490
- if (decoded.length === 0) {
12491
- throw new Error("Invalid base64 audio data received from OpenRouter");
12492
- }
12493
- audioChunks.push(decoded);
12494
- }
12495
- const audioBuffer = Buffer.concat(audioChunks);
12496
- if (audioBuffer.length === 0) {
12497
- throw new Error(
12498
- "OpenRouter TTS returned no audio data. The model may have failed to generate audio or the stream was interrupted."
14616
+ const tools = this.resolveTools(options, spec);
14617
+ return { ...options, background, tools };
14618
+ }
14619
+ resolveTools(options, spec) {
14620
+ if (options.tools === void 0) {
14621
+ return spec.requiredTools;
14622
+ }
14623
+ for (const tool of options.tools) {
14624
+ if (!spec.capabilities.tools.includes(tool.type)) {
14625
+ throw new ResearchValidationError(
14626
+ `Model "${spec.modelId}" does not accept the "${tool.type}" tool. ` + (spec.capabilities.tools.length > 0 ? `Accepted tools: ${spec.capabilities.tools.join(", ")}.` : "This model manages its research tools itself \u2014 omit the tools option.")
12499
14627
  );
12500
14628
  }
12501
- const cost = calculateOpenRouterSpeechCost(bareModelId, options.input.length);
12502
- return {
12503
- // Use Uint8Array for clean ArrayBuffer extraction (safer than buffer.slice for Node.js Buffer)
12504
- audio: new Uint8Array(audioBuffer).buffer,
12505
- model: options.model,
12506
- usage: {
12507
- characterCount: options.input.length
12508
- },
12509
- cost,
12510
- format: format2
12511
- };
12512
- } catch (error) {
12513
- const err = error;
12514
- const apiError = err.error?.message || err.error?.code || "";
12515
- const bodyInfo = err.body ? JSON.stringify(err.body) : "";
12516
- const details = apiError || bodyInfo || err.message || "Unknown error";
12517
- logger.debug("TTS error", {
12518
- model: bareModelId,
12519
- voice,
12520
- format: format2,
12521
- status: err.status,
12522
- error: err.error,
12523
- body: err.body,
12524
- message: err.message
14629
+ }
14630
+ const hasDataSource = options.tools.some(
14631
+ (tool) => RESEARCH_DATA_SOURCE_TOOL_TYPES.includes(tool.type)
14632
+ );
14633
+ if (!hasDataSource && spec.requiredTools) {
14634
+ return [...spec.requiredTools, ...options.tools];
14635
+ }
14636
+ return options.tools;
14637
+ }
14638
+ enforceLifecycle(spec) {
14639
+ const shutdownDate = spec.metadata?.shutdownDate;
14640
+ if (!shutdownDate) {
14641
+ return;
14642
+ }
14643
+ const shutdownMs = Date.parse(shutdownDate);
14644
+ if (Number.isNaN(shutdownMs)) {
14645
+ return;
14646
+ }
14647
+ const nowMs = this.now();
14648
+ if (nowMs >= shutdownMs) {
14649
+ throw new ResearchDeprecatedModelError({
14650
+ modelId: spec.modelId,
14651
+ shutdownDate,
14652
+ replacement: spec.metadata?.replacement
12525
14653
  });
12526
- throw new Error(
12527
- `OpenRouter TTS failed for model ${bareModelId}: ${details}` + (err.status ? ` (HTTP ${err.status})` : "")
14654
+ }
14655
+ const warningStartMs = shutdownMs - RESEARCH_SHUTDOWN_WARNING_WINDOW_DAYS * MS_PER_DAY;
14656
+ if (nowMs >= warningStartMs) {
14657
+ this.logger.warn(
14658
+ `Research model "${spec.modelId}" shuts down on ${shutdownDate}` + (spec.metadata?.replacement ? ` \u2014 migrate to "${spec.metadata.replacement}".` : ".")
12528
14659
  );
12529
14660
  }
12530
14661
  }
@@ -12532,36 +14663,6 @@ Original error: ${error.message}`
12532
14663
  }
12533
14664
  });
12534
14665
 
12535
- // src/providers/discovery.ts
12536
- function discoverProviderAdapters() {
12537
- const adapters = [];
12538
- for (const discover of DISCOVERERS) {
12539
- const adapter = discover();
12540
- if (adapter) {
12541
- adapters.push(adapter);
12542
- }
12543
- }
12544
- return adapters;
12545
- }
12546
- var DISCOVERERS;
12547
- var init_discovery = __esm({
12548
- "src/providers/discovery.ts"() {
12549
- "use strict";
12550
- init_anthropic();
12551
- init_gemini();
12552
- init_huggingface();
12553
- init_openai();
12554
- init_openrouter();
12555
- DISCOVERERS = [
12556
- createOpenAIProviderFromEnv,
12557
- createAnthropicProviderFromEnv,
12558
- createGeminiProviderFromEnv,
12559
- createHuggingFaceProviderFromEnv,
12560
- createOpenRouterProviderFromEnv
12561
- ];
12562
- }
12563
- });
12564
-
12565
14666
  // src/core/model-registry.ts
12566
14667
  var ModelRegistry;
12567
14668
  var init_model_registry = __esm({
@@ -12786,6 +14887,8 @@ var init_image = __esm({
12786
14887
  this.adapters = adapters;
12787
14888
  this.defaultProvider = defaultProvider;
12788
14889
  }
14890
+ adapters;
14891
+ defaultProvider;
12789
14892
  /**
12790
14893
  * Generate images from a text prompt.
12791
14894
  *
@@ -12838,6 +14941,8 @@ var init_speech = __esm({
12838
14941
  this.adapters = adapters;
12839
14942
  this.defaultProvider = defaultProvider;
12840
14943
  }
14944
+ adapters;
14945
+ defaultProvider;
12841
14946
  /**
12842
14947
  * Generate speech audio from text.
12843
14948
  *
@@ -12933,6 +15038,7 @@ var init_text = __esm({
12933
15038
  constructor(client) {
12934
15039
  this.client = client;
12935
15040
  }
15041
+ client;
12936
15042
  /**
12937
15043
  * Generate a complete text response.
12938
15044
  *
@@ -12968,6 +15074,7 @@ var init_vision = __esm({
12968
15074
  constructor(client) {
12969
15075
  this.client = client;
12970
15076
  }
15077
+ client;
12971
15078
  /**
12972
15079
  * Build a message builder with the image content attached.
12973
15080
  * Handles URLs, data URLs, base64 strings, and binary buffers.
@@ -13094,6 +15201,7 @@ var init_options = __esm({
13094
15201
  constructor(defaultProvider = "openai") {
13095
15202
  this.defaultProvider = defaultProvider;
13096
15203
  }
15204
+ defaultProvider;
13097
15205
  parse(identifier) {
13098
15206
  const trimmed = identifier.trim();
13099
15207
  if (!trimmed) {
@@ -13125,6 +15233,7 @@ var init_client = __esm({
13125
15233
  "use strict";
13126
15234
  init_builder();
13127
15235
  init_discovery();
15236
+ init_namespace();
13128
15237
  init_constants();
13129
15238
  init_model_registry();
13130
15239
  init_image();
@@ -13143,6 +15252,12 @@ var init_client = __esm({
13143
15252
  image;
13144
15253
  speech;
13145
15254
  vision;
15255
+ /**
15256
+ * Deep research — long-running, server-side research jobs with cited
15257
+ * reports (OpenAI Responses, Gemini Interactions, OpenRouter research
15258
+ * models). See docs/specs/002-deep-research.md.
15259
+ */
15260
+ research;
13146
15261
  constructor(...args) {
13147
15262
  let adapters = [];
13148
15263
  let defaultProvider;
@@ -13194,6 +15309,7 @@ var init_client = __esm({
13194
15309
  this.image = new ImageNamespace(this.adapters, this.defaultProvider);
13195
15310
  this.speech = new SpeechNamespace(this.adapters, this.defaultProvider);
13196
15311
  this.vision = new VisionNamespace(this);
15312
+ this.research = new ResearchNamespace(this.adapters, this.parser);
13197
15313
  }
13198
15314
  stream(options) {
13199
15315
  const descriptor = this.parser.parse(options.model);
@@ -13460,8 +15576,8 @@ var init_builder = __esm({
13460
15576
  return this;
13461
15577
  }
13462
15578
  /** Set maximum iterations. */
13463
- withMaxIterations(max) {
13464
- this.core.maxIterations = max;
15579
+ withMaxIterations(max2) {
15580
+ this.core.maxIterations = max2;
13465
15581
  return this;
13466
15582
  }
13467
15583
  /** Set the budget limit in USD. */
@@ -13590,10 +15706,10 @@ var init_builder = __esm({
13590
15706
  return this;
13591
15707
  }
13592
15708
  /** Set the maximum number of gadgets to execute per LLM response. */
13593
- withMaxGadgetsPerResponse(max) {
13594
- if (max < 0) throw new Error("maxGadgetsPerResponse must be a non-negative number");
13595
- if (!Number.isInteger(max)) throw new Error("maxGadgetsPerResponse must be an integer");
13596
- this.gadgets.maxGadgetsPerResponse = max;
15709
+ withMaxGadgetsPerResponse(max2) {
15710
+ if (max2 < 0) throw new Error("maxGadgetsPerResponse must be a non-negative number");
15711
+ if (!Number.isInteger(max2)) throw new Error("maxGadgetsPerResponse must be an integer");
15712
+ this.gadgets.maxGadgetsPerResponse = max2;
13597
15713
  return this;
13598
15714
  }
13599
15715
  /** Enable or disable gadget output limiting. */
@@ -14294,6 +16410,8 @@ var init_cost_reporting_client = __esm({
14294
16410
  }
14295
16411
  };
14296
16412
  }
16413
+ client;
16414
+ reportCost;
14297
16415
  image;
14298
16416
  speech;
14299
16417
  /**
@@ -14570,13 +16688,22 @@ var init_parser2 = __esm({
14570
16688
  GadgetCallParser = class {
14571
16689
  buffer = "";
14572
16690
  lastEmittedTextOffset = 0;
16691
+ /** Non-null only while a single trailing gadget block is mid-stream. */
16692
+ currentPartial = null;
14573
16693
  startPrefix;
14574
16694
  endPrefix;
14575
16695
  argPrefix;
16696
+ /** Length of the longest marker; `maxMarkerLength - 1` is the scan-resume overlap. */
16697
+ maxMarkerLength;
14576
16698
  constructor(options = {}) {
14577
16699
  this.startPrefix = options.startPrefix ?? GADGET_START_PREFIX;
14578
16700
  this.endPrefix = options.endPrefix ?? GADGET_END_PREFIX;
14579
16701
  this.argPrefix = options.argPrefix ?? GADGET_ARG_PREFIX;
16702
+ this.maxMarkerLength = Math.max(
16703
+ this.startPrefix.length,
16704
+ this.endPrefix.length,
16705
+ this.argPrefix.length
16706
+ );
14580
16707
  }
14581
16708
  /**
14582
16709
  * Extract and consume text up to the given index.
@@ -14663,12 +16790,13 @@ var init_parser2 = __esm({
14663
16790
  const metadataEndIndex = this.buffer.indexOf("\n", metadataStartIndex);
14664
16791
  if (metadataEndIndex === -1) break;
14665
16792
  const headerLine = this.buffer.substring(metadataStartIndex, metadataEndIndex).trim();
14666
- const { gadgetName, invocationId, dependencies } = this.parseInvocationMetadata(headerLine);
16793
+ const { gadgetName, invocationId, dependencies } = this.currentPartial ? this.currentPartial : this.parseInvocationMetadata(headerLine);
14667
16794
  const contentStartIndex = metadataEndIndex + 1;
14668
16795
  let partEndIndex;
14669
16796
  let endMarkerLength = 0;
14670
- const nextStartPos = this.buffer.indexOf(this.startPrefix, contentStartIndex);
14671
- const endPos = this.buffer.indexOf(this.endPrefix, contentStartIndex);
16797
+ const bodySearchStart = this.currentPartial ? contentStartIndex + Math.max(0, this.currentPartial.bodyScannedLen - (this.maxMarkerLength - 1)) : contentStartIndex;
16798
+ const nextStartPos = this.buffer.indexOf(this.startPrefix, bodySearchStart);
16799
+ const endPos = this.buffer.indexOf(this.endPrefix, bodySearchStart);
14672
16800
  if (nextStartPos !== -1 && (endPos === -1 || nextStartPos < endPos)) {
14673
16801
  partEndIndex = nextStartPos;
14674
16802
  endMarkerLength = 0;
@@ -14676,9 +16804,22 @@ var init_parser2 = __esm({
14676
16804
  partEndIndex = endPos;
14677
16805
  endMarkerLength = this.endPrefix.length;
14678
16806
  } else {
16807
+ if (!this.currentPartial) {
16808
+ this.currentPartial = this.newPartialState(gadgetName, invocationId, dependencies);
16809
+ }
16810
+ yield* this.emitArgPartials(
16811
+ this.currentPartial,
16812
+ contentStartIndex,
16813
+ this.buffer.length,
16814
+ false
16815
+ );
14679
16816
  break;
14680
16817
  }
14681
- const parametersRaw = this.buffer.substring(contentStartIndex, partEndIndex).trim();
16818
+ const rawSlice = this.buffer.substring(contentStartIndex, partEndIndex);
16819
+ if (this.currentPartial) {
16820
+ yield* this.emitArgPartials(this.currentPartial, contentStartIndex, partEndIndex, true);
16821
+ }
16822
+ const parametersRaw = rawSlice.trim();
14682
16823
  const { parameters, parseError } = this.parseParameters(parametersRaw);
14683
16824
  yield {
14684
16825
  type: "gadget_call",
@@ -14691,6 +16832,7 @@ var init_parser2 = __esm({
14691
16832
  dependencies
14692
16833
  }
14693
16834
  };
16835
+ this.currentPartial = null;
14694
16836
  startIndex = partEndIndex + endMarkerLength;
14695
16837
  this.lastEmittedTextOffset = startIndex;
14696
16838
  }
@@ -14699,6 +16841,117 @@ var init_parser2 = __esm({
14699
16841
  this.lastEmittedTextOffset = 0;
14700
16842
  }
14701
16843
  }
16844
+ /** Create fresh partial-tracking state for a newly-started streaming gadget. */
16845
+ newPartialState(gadgetName, invocationId, dependencies) {
16846
+ return {
16847
+ gadgetName,
16848
+ invocationId,
16849
+ dependencies,
16850
+ emittedFieldLengths: /* @__PURE__ */ new Map(),
16851
+ completedFields: /* @__PURE__ */ new Set(),
16852
+ bodyScannedLen: 0,
16853
+ lastArgRelOffset: -1
16854
+ };
16855
+ }
16856
+ /**
16857
+ * Emit per-field "growing value" partials for an in-progress (or, when
16858
+ * `allComplete`, a just-completed) gadget body delimited by [bodyStart, bodyEnd).
16859
+ *
16860
+ * Incremental by design: each call resumes the `!!!ARG:` scan near where the last
16861
+ * one stopped (backing off by one marker's worth so a marker split across a chunk
16862
+ * boundary is still found) and only re-touches the in-progress field, so a long
16863
+ * streamed body costs O(new bytes) per feed instead of O(body). Every field except
16864
+ * the in-progress (last) one is complete — a following `!!!ARG:` terminated it; the
16865
+ * last field is tentative unless `allComplete`. The tentative field holds back any
16866
+ * suffix that is a partial prefix of a gadget marker so it never leaks into a value.
16867
+ *
16868
+ * We deliberately do NOT run stripMarkdownFences here: an unbalanced opening fence
16869
+ * sits before the first `!!!ARG:` (never emitted) and the authoritative gadget_call
16870
+ * still strips fences from the full raw parameters.
16871
+ */
16872
+ *emitArgPartials(state, bodyStart, bodyEnd, allComplete) {
16873
+ const argLen = this.argPrefix.length;
16874
+ let searchAbs = bodyStart + Math.max(0, state.bodyScannedLen - (this.maxMarkerLength - 1));
16875
+ if (state.lastArgRelOffset >= 0) {
16876
+ searchAbs = Math.max(searchAbs, bodyStart + state.lastArgRelOffset + argLen);
16877
+ }
16878
+ while (true) {
16879
+ const argAbs = this.buffer.indexOf(this.argPrefix, searchAbs);
16880
+ if (argAbs === -1 || argAbs >= bodyEnd) break;
16881
+ if (state.lastArgRelOffset >= 0) {
16882
+ yield* this.emitFieldRange(state, bodyStart + state.lastArgRelOffset, argAbs, true);
16883
+ }
16884
+ state.lastArgRelOffset = argAbs - bodyStart;
16885
+ searchAbs = argAbs + argLen;
16886
+ }
16887
+ if (state.lastArgRelOffset >= 0) {
16888
+ yield* this.emitFieldRange(state, bodyStart + state.lastArgRelOffset, bodyEnd, allComplete);
16889
+ }
16890
+ state.bodyScannedLen = bodyEnd - bodyStart;
16891
+ }
16892
+ /**
16893
+ * Emit a single field whose `!!!ARG:` marker starts at `markerAbs` and whose value
16894
+ * runs to `valueEndAbs` (the next marker, or the body end). Mirrors the per-field
16895
+ * semantics of the old split-based emitter: field-path line, hold-back for a
16896
+ * tentative value, single trailing-newline strip.
16897
+ */
16898
+ *emitFieldRange(state, markerAbs, valueEndAbs, complete2) {
16899
+ const pathStart = markerAbs + this.argPrefix.length;
16900
+ const newlineAbs = this.buffer.indexOf("\n", pathStart);
16901
+ if (newlineAbs === -1 || newlineAbs >= valueEndAbs) {
16902
+ if (!complete2) return;
16903
+ const pointer = this.buffer.substring(pathStart, valueEndAbs).trim();
16904
+ if (pointer) yield* this.emitFieldDelta(state, pointer, "", true);
16905
+ return;
16906
+ }
16907
+ const fieldPath = this.buffer.substring(pathStart, newlineAbs).trim();
16908
+ if (!fieldPath) return;
16909
+ let value = this.buffer.substring(newlineAbs + 1, valueEndAbs);
16910
+ if (!complete2) {
16911
+ const hold = this.trailingPartialMarkerLength(value);
16912
+ if (hold > 0) value = value.slice(0, value.length - hold);
16913
+ }
16914
+ if (value.endsWith("\n")) value = value.slice(0, -1);
16915
+ yield* this.emitFieldDelta(state, fieldPath, value, complete2);
16916
+ }
16917
+ /**
16918
+ * Emit a single partial for a field, but only when its value grew or it newly
16919
+ * completed — keeping event volume proportional to field growth, not characters.
16920
+ */
16921
+ *emitFieldDelta(state, fieldPath, value, fieldComplete) {
16922
+ const previousLength = state.emittedFieldLengths.get(fieldPath) ?? -1;
16923
+ const grew = value.length > previousLength;
16924
+ const newlyComplete = fieldComplete && !state.completedFields.has(fieldPath);
16925
+ if (!grew && !newlyComplete) return;
16926
+ const delta = previousLength < 0 ? value : value.slice(Math.min(previousLength, value.length));
16927
+ state.emittedFieldLengths.set(fieldPath, value.length);
16928
+ if (fieldComplete) state.completedFields.add(fieldPath);
16929
+ yield {
16930
+ type: "gadget_args_partial",
16931
+ invocationId: state.invocationId,
16932
+ gadgetName: state.gadgetName,
16933
+ fieldPath,
16934
+ value,
16935
+ delta,
16936
+ isFieldComplete: fieldComplete
16937
+ };
16938
+ }
16939
+ /**
16940
+ * Length of the longest suffix of `value` that is a proper prefix of any gadget
16941
+ * marker (start/end/arg). Used to hold back the beginning of an incoming marker
16942
+ * so it never appears inside a streamed value.
16943
+ */
16944
+ trailingPartialMarkerLength(value) {
16945
+ const markers = [this.startPrefix, this.endPrefix, this.argPrefix];
16946
+ const limit = Math.min(this.maxMarkerLength - 1, value.length);
16947
+ for (let len = limit; len >= 1; len--) {
16948
+ const suffix = value.slice(value.length - len);
16949
+ for (const marker of markers) {
16950
+ if (suffix.length < marker.length && marker.startsWith(suffix)) return len;
16951
+ }
16952
+ }
16953
+ return 0;
16954
+ }
14702
16955
  // Finalize parsing and return remaining text or incomplete gadgets
14703
16956
  *finalize() {
14704
16957
  const startIndex = this.buffer.indexOf(this.startPrefix, this.lastEmittedTextOffset);
@@ -14711,9 +16964,18 @@ var init_parser2 = __esm({
14711
16964
  const metadataEndIndex = this.buffer.indexOf("\n", metadataStartIndex);
14712
16965
  if (metadataEndIndex !== -1) {
14713
16966
  const headerLine = this.buffer.substring(metadataStartIndex, metadataEndIndex).trim();
14714
- const { gadgetName, invocationId, dependencies } = this.parseInvocationMetadata(headerLine);
16967
+ const { gadgetName, invocationId, dependencies } = this.currentPartial ? this.currentPartial : this.parseInvocationMetadata(headerLine);
14715
16968
  const contentStartIndex = metadataEndIndex + 1;
14716
- const parametersRaw = this.buffer.substring(contentStartIndex).trim();
16969
+ const contentRaw = this.buffer.substring(contentStartIndex);
16970
+ if (this.currentPartial) {
16971
+ yield* this.emitArgPartials(
16972
+ this.currentPartial,
16973
+ contentStartIndex,
16974
+ this.buffer.length,
16975
+ true
16976
+ );
16977
+ }
16978
+ const parametersRaw = contentRaw.trim();
14717
16979
  const { parameters, parseError } = this.parseParameters(parametersRaw);
14718
16980
  yield {
14719
16981
  type: "gadget_call",
@@ -14726,6 +16988,7 @@ var init_parser2 = __esm({
14726
16988
  dependencies
14727
16989
  }
14728
16990
  };
16991
+ this.currentPartial = null;
14729
16992
  return;
14730
16993
  }
14731
16994
  }
@@ -14738,6 +17001,7 @@ var init_parser2 = __esm({
14738
17001
  reset() {
14739
17002
  this.buffer = "";
14740
17003
  this.lastEmittedTextOffset = 0;
17004
+ this.currentPartial = null;
14741
17005
  }
14742
17006
  };
14743
17007
  }
@@ -15663,6 +17927,29 @@ async function notifyGadgetStart(ctx) {
15663
17927
  await safeObserve(() => hookFn(context), ctx.logger);
15664
17928
  }
15665
17929
  }
17930
+ async function notifyGadgetArgsPartial(ctx) {
17931
+ if (!ctx.hooks?.onGadgetArgsPartial && !ctx.parentObservers?.onGadgetArgsPartial) return;
17932
+ const subagentContext = ctx.tree && ctx.parentNodeId ? getSubagentContextForNode(ctx.tree, ctx.parentNodeId) : void 0;
17933
+ const context = {
17934
+ iteration: ctx.iteration,
17935
+ invocationId: ctx.event.invocationId,
17936
+ gadgetName: ctx.event.gadgetName,
17937
+ fieldPath: ctx.event.fieldPath,
17938
+ value: ctx.event.value,
17939
+ delta: ctx.event.delta,
17940
+ isFieldComplete: ctx.event.isFieldComplete,
17941
+ logger: ctx.logger,
17942
+ subagentContext
17943
+ };
17944
+ if (ctx.hooks?.onGadgetArgsPartial) {
17945
+ const hookFn = ctx.hooks.onGadgetArgsPartial;
17946
+ await safeObserve(() => hookFn(context), ctx.logger);
17947
+ }
17948
+ if (ctx.parentObservers?.onGadgetArgsPartial) {
17949
+ const hookFn = ctx.parentObservers.onGadgetArgsPartial;
17950
+ await safeObserve(() => hookFn(context), ctx.logger);
17951
+ }
17952
+ }
15666
17953
  async function notifyGadgetComplete(ctx) {
15667
17954
  const gadgetNode = ctx.tree?.getNodeByInvocationId(ctx.invocationId);
15668
17955
  const subagentContext = ctx.tree && gadgetNode ? getSubagentContextForNode(ctx.tree, gadgetNode.id) : void 0;
@@ -16471,6 +18758,7 @@ var init_stream_processor = __esm({
16471
18758
  init_gadget_dispatcher();
16472
18759
  init_gadget_hook_lifecycle();
16473
18760
  init_gadget_limit_guard();
18761
+ init_observer_notifier();
16474
18762
  init_safe_observe();
16475
18763
  StreamProcessor = class {
16476
18764
  iteration;
@@ -16479,6 +18767,10 @@ var init_stream_processor = __esm({
16479
18767
  parser;
16480
18768
  // Execution Tree context
16481
18769
  tree;
18770
+ /** LLM-call node these gadgets hang off; used to derive subagentContext for partials. */
18771
+ parentNodeId;
18772
+ /** Parent agent observers (subagent visibility) — also notified for arg partials. */
18773
+ parentObservers;
16482
18774
  responseText = "";
16483
18775
  // Dependency resolution is delegated to GadgetDependencyResolver
16484
18776
  dependencyResolver;
@@ -16492,6 +18784,8 @@ var init_stream_processor = __esm({
16492
18784
  this.hooks = options.hooks ?? {};
16493
18785
  this.logger = options.logger ?? createLogger({ name: "llmist:stream-processor" });
16494
18786
  this.tree = options.tree;
18787
+ this.parentNodeId = options.parentNodeId;
18788
+ this.parentObservers = options.parentObservers;
16495
18789
  this.dependencyResolver = new GadgetDependencyResolver({
16496
18790
  priorCompletedInvocations: options.priorCompletedInvocations,
16497
18791
  priorFailedInvocations: options.priorFailedInvocations
@@ -16693,6 +18987,17 @@ var init_stream_processor = __esm({
16693
18987
  for await (const e of this.dispatcher.dispatch(event.call)) {
16694
18988
  yield e;
16695
18989
  }
18990
+ } else if (event.type === "gadget_args_partial") {
18991
+ await notifyGadgetArgsPartial({
18992
+ tree: this.tree,
18993
+ hooks: this.hooks.observers,
18994
+ parentObservers: this.parentObservers,
18995
+ logger: this.logger,
18996
+ iteration: this.iteration,
18997
+ parentNodeId: this.parentNodeId,
18998
+ event
18999
+ });
19000
+ yield event;
16696
19001
  } else {
16697
19002
  yield event;
16698
19003
  }
@@ -16864,7 +19169,7 @@ var init_stream_processor_factory = __esm({
16864
19169
 
16865
19170
  // src/mcp/errors.ts
16866
19171
  var McpError, McpUntrustedCommandError, McpConnectError, McpToolCallError, McpTimeoutError, JsonSchemaConversionError;
16867
- var init_errors2 = __esm({
19172
+ var init_errors3 = __esm({
16868
19173
  "src/mcp/errors.ts"() {
16869
19174
  "use strict";
16870
19175
  McpError = class extends Error {
@@ -16950,7 +19255,7 @@ var init_allowlist = __esm({
16950
19255
  "src/mcp/allowlist.ts"() {
16951
19256
  "use strict";
16952
19257
  import_node_path6 = __toESM(require("path"), 1);
16953
- init_errors2();
19258
+ init_errors3();
16954
19259
  DEFAULT_MCP_COMMAND_ALLOWLIST = /* @__PURE__ */ new Set([
16955
19260
  "npx",
16956
19261
  "node",
@@ -16988,7 +19293,7 @@ var init_client2 = __esm({
16988
19293
  "src/mcp/client.ts"() {
16989
19294
  "use strict";
16990
19295
  init_allowlist();
16991
- init_errors2();
19296
+ init_errors3();
16992
19297
  cachedSdk = null;
16993
19298
  DEFAULT_CLIENT_INFO = { name: "llmist", version: "0.0.0" };
16994
19299
  McpClient = class {
@@ -16997,6 +19302,7 @@ var init_client2 = __esm({
16997
19302
  this.injectedTransport = opts?.transport;
16998
19303
  this.clientInfo = opts?.clientInfo ?? DEFAULT_CLIENT_INFO;
16999
19304
  }
19305
+ spec;
17000
19306
  sdkClient = null;
17001
19307
  spawnedPid = null;
17002
19308
  closed = false;
@@ -17570,7 +19876,7 @@ var init_json_schema_to_zod = __esm({
17570
19876
  "src/mcp/json-schema-to-zod.ts"() {
17571
19877
  "use strict";
17572
19878
  import_zod4 = require("zod");
17573
- init_errors2();
19879
+ init_errors3();
17574
19880
  }
17575
19881
  });
17576
19882
 
@@ -18610,7 +20916,17 @@ __export(index_exports, {
18610
20916
  OpenAIChatProvider: () => OpenAIChatProvider,
18611
20917
  OpenAICompatibleProvider: () => OpenAICompatibleProvider,
18612
20918
  OpenRouterProvider: () => OpenRouterProvider,
20919
+ RESEARCH_DATA_SOURCE_TOOL_TYPES: () => RESEARCH_DATA_SOURCE_TOOL_TYPES,
18613
20920
  RateLimitTracker: () => RateLimitTracker,
20921
+ ResearchDeprecatedModelError: () => ResearchDeprecatedModelError,
20922
+ ResearchJobNotResumableError: () => ResearchJobNotResumableError,
20923
+ ResearchNamespace: () => ResearchNamespace,
20924
+ ResearchNotPollableError: () => ResearchNotPollableError,
20925
+ ResearchNotSupportedError: () => ResearchNotSupportedError,
20926
+ ResearchResultCollector: () => ResearchResultCollector,
20927
+ ResearchStreamConsumedError: () => ResearchStreamConsumedError,
20928
+ ResearchTimeoutError: () => ResearchTimeoutError,
20929
+ ResearchValidationError: () => ResearchValidationError,
18614
20930
  SimpleSessionManager: () => SimpleSessionManager,
18615
20931
  Skill: () => Skill,
18616
20932
  SkillRegistry: () => SkillRegistry,
@@ -18644,6 +20960,7 @@ __export(index_exports, {
18644
20960
  detectImageMimeType: () => detectImageMimeType,
18645
20961
  discoverProviderAdapters: () => discoverProviderAdapters,
18646
20962
  discoverSkills: () => discoverSkills,
20963
+ estimateResearchCost: () => estimateResearchCost,
18647
20964
  extractMessageText: () => extractMessageText,
18648
20965
  extractRetryAfterMs: () => extractRetryAfterMs,
18649
20966
  filterByDepth: () => filterByDepth,
@@ -18923,7 +21240,7 @@ init_typed_gadget();
18923
21240
  // src/mcp/index.ts
18924
21241
  init_allowlist();
18925
21242
  init_client2();
18926
- init_errors2();
21243
+ init_errors3();
18927
21244
 
18928
21245
  // src/mcp/gadget-exporter.ts
18929
21246
  init_schema_to_json();
@@ -19164,6 +21481,14 @@ init_tool_adapter();
19164
21481
  // src/index.ts
19165
21482
  init_constants2();
19166
21483
 
21484
+ // src/research/index.ts
21485
+ init_collector();
21486
+ init_constants3();
21487
+ init_cost();
21488
+ init_errors2();
21489
+ init_namespace();
21490
+ init_types();
21491
+
19167
21492
  // src/utils/config-resolver.ts
19168
21493
  function resolveValue(ctx, gadgetName, options) {
19169
21494
  const { runtime, subagentKey, parentKey, defaultValue, handleInherit } = options;
@@ -19462,11 +21787,11 @@ var format = {
19462
21787
  };
19463
21788
 
19464
21789
  // src/utils/timing.ts
19465
- function randomDelay(min, max) {
19466
- return Math.floor(Math.random() * (max - min + 1)) + min;
21790
+ function randomDelay(min, max2) {
21791
+ return Math.floor(Math.random() * (max2 - min + 1)) + min;
19467
21792
  }
19468
- async function humanDelay(min = 50, max = 150) {
19469
- const delay = randomDelay(min, max);
21793
+ async function humanDelay(min = 50, max2 = 150) {
21794
+ const delay = randomDelay(min, max2);
19470
21795
  return new Promise((resolve2) => setTimeout(resolve2, delay));
19471
21796
  }
19472
21797
  async function withTimeout(fn, timeoutMs, signal) {
@@ -19606,7 +21931,17 @@ function getHostExports2(ctx) {
19606
21931
  OpenAIChatProvider,
19607
21932
  OpenAICompatibleProvider,
19608
21933
  OpenRouterProvider,
21934
+ RESEARCH_DATA_SOURCE_TOOL_TYPES,
19609
21935
  RateLimitTracker,
21936
+ ResearchDeprecatedModelError,
21937
+ ResearchJobNotResumableError,
21938
+ ResearchNamespace,
21939
+ ResearchNotPollableError,
21940
+ ResearchNotSupportedError,
21941
+ ResearchResultCollector,
21942
+ ResearchStreamConsumedError,
21943
+ ResearchTimeoutError,
21944
+ ResearchValidationError,
19610
21945
  SimpleSessionManager,
19611
21946
  Skill,
19612
21947
  SkillRegistry,
@@ -19640,6 +21975,7 @@ function getHostExports2(ctx) {
19640
21975
  detectImageMimeType,
19641
21976
  discoverProviderAdapters,
19642
21977
  discoverSkills,
21978
+ estimateResearchCost,
19643
21979
  extractMessageText,
19644
21980
  extractRetryAfterMs,
19645
21981
  filterByDepth,