noosphere 0.9.1 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -694,35 +694,181 @@ const claude = await countTokensAnthropic(messages, ANTHROPIC_KEY, 'claude-sonne
694
694
 
695
695
  ## Agent Loop & pi-ai Access
696
696
 
697
- Noosphere re-exports the full [pi-ai](https://github.com/nicholasgriffintn/pi-ai) library for direct access to agent loops, tool calling, cost calculation, and streaming APIs.
697
+ Noosphere re-exports the full [pi-ai](https://github.com/mariozechner/pi-ai) library for direct access to agent loops, tool calling, cost calculation, and streaming APIs.
698
+
699
+ ### Preprocessor — Context Compaction
700
+
701
+ The preprocessor hook runs **before every LLM call** in the agent loop. Use it to manage context window limits — truncate old messages, summarize conversations, or implement sliding window strategies.
698
702
 
699
703
  ```typescript
700
- import {
701
- agentLoop, calculateCost,
702
- piStream, piComplete, piStreamSimple, piCompleteSimple,
703
- setApiKey, getApiKey, getPiModel, getPiModels, getPiProviders,
704
- } from 'noosphere';
704
+ import { agentLoop, getPiModel, setApiKey, countTokensOpenAI } from 'noosphere';
705
+ import type { AgentLoopConfig, AgentContext, PiMessage } from 'noosphere';
705
706
 
706
- // Agent loop with tool calling and preprocessor (compaction hook)
707
- import type { AgentLoopConfig, AgentContext, AgentTool } from 'noosphere';
707
+ setApiKey('openai', process.env.OPENAI_API_KEY!);
708
708
 
709
709
  const config: AgentLoopConfig = {
710
710
  model: getPiModel('openai', 'gpt-4o'),
711
- // Preprocessor runs before each LLM call — use for context compaction
711
+
712
+ // Preprocessor runs before each LLM call
712
713
  preprocessor: async (messages) => {
713
- // Truncate old messages, summarize, etc.
714
+ // Strategy 1: Simple sliding window — keep last N messages
714
715
  if (messages.length > 50) {
715
- return messages.slice(-20); // keep last 20
716
+ return messages.slice(-20);
716
717
  }
717
718
  return messages;
718
719
  },
719
720
  };
720
721
 
721
- // Calculate cost before sending
722
+ // Start the agent loop
723
+ const context: AgentContext = {
724
+ systemPrompt: 'You are a helpful assistant.',
725
+ messages: [],
726
+ };
727
+
728
+ const userMessage = {
729
+ role: 'user' as const,
730
+ content: 'Hello!',
731
+ timestamp: Date.now(),
732
+ };
733
+
734
+ const stream = agentLoop(userMessage, context, config);
735
+
736
+ for await (const event of stream) {
737
+ if (event.type === 'message_update' && event.assistantMessageEvent.type === 'text_delta') {
738
+ process.stdout.write(event.assistantMessageEvent.delta);
739
+ }
740
+ }
741
+ ```
742
+
743
+ #### Preprocessor Strategies
744
+
745
+ **Token-aware compaction** — count tokens and trim to fit the context window:
746
+
747
+ ```typescript
748
+ preprocessor: async (messages) => {
749
+ const model = getPiModel('openai', 'gpt-4o');
750
+ const maxTokens = model.contextWindow * 0.8; // leave 20% for response
751
+
752
+ let totalTokens = 0;
753
+ const kept: PiMessage[] = [];
754
+
755
+ // Keep messages from newest to oldest until we hit the limit
756
+ for (let i = messages.length - 1; i >= 0; i--) {
757
+ const msg = messages[i];
758
+ const content = 'content' in msg && typeof msg.content === 'string' ? msg.content : '';
759
+ const msgTokens = countTokensOpenAI([{ role: msg.role, content }], 'gpt-4o');
760
+
761
+ if (totalTokens + msgTokens > maxTokens) break;
762
+ totalTokens += msgTokens;
763
+ kept.unshift(msg);
764
+ }
765
+
766
+ return kept;
767
+ },
768
+ ```
769
+
770
+ **Summarization compaction** — summarize old messages, keep recent ones:
771
+
772
+ ```typescript
773
+ preprocessor: async (messages) => {
774
+ if (messages.length <= 20) return messages;
775
+
776
+ // Summarize the older messages using the LLM itself
777
+ const oldMessages = messages.slice(0, -10);
778
+ const recentMessages = messages.slice(-10);
779
+
780
+ const summary = await piCompleteSimple(getPiModel('openai', 'gpt-4o-mini'), {
781
+ systemPrompt: 'Summarize this conversation in 2-3 sentences.',
782
+ messages: oldMessages,
783
+ });
784
+
785
+ // Replace old messages with a summary message
786
+ const summaryText = summary.content
787
+ .filter((c): c is { type: 'text'; text: string } => c.type === 'text')
788
+ .map((c) => c.text)
789
+ .join('');
790
+
791
+ return [
792
+ { role: 'user' as const, content: `[Previous conversation summary: ${summaryText}]`, timestamp: Date.now() },
793
+ ...recentMessages,
794
+ ];
795
+ },
796
+ ```
797
+
798
+ ### Tool Calling
799
+
800
+ Define tools for the agent loop with typed parameters:
801
+
802
+ ```typescript
803
+ import { Type } from '@sinclair/typebox';
804
+ import type { AgentTool } from 'noosphere';
805
+
806
+ const weatherTool: AgentTool = {
807
+ name: 'get_weather',
808
+ label: 'Get Weather',
809
+ description: 'Get the current weather for a location',
810
+ parameters: Type.Object({
811
+ location: Type.String({ description: 'City name' }),
812
+ }),
813
+ execute: async (toolCallId, params) => {
814
+ const weather = await fetchWeather(params.location);
815
+ return { output: JSON.stringify(weather), details: weather };
816
+ },
817
+ };
818
+
819
+ const context: AgentContext = {
820
+ systemPrompt: 'You are a helpful assistant with weather access.',
821
+ messages: [],
822
+ tools: [weatherTool],
823
+ };
824
+
825
+ const stream = agentLoop(userMessage, context, config);
826
+
827
+ for await (const event of stream) {
828
+ if (event.type === 'tool_execution_start') {
829
+ console.log(`Calling ${event.toolName}...`);
830
+ }
831
+ if (event.type === 'tool_execution_end') {
832
+ console.log(`Result: ${event.result}`);
833
+ }
834
+ }
835
+ ```
836
+
837
+ ### Cost Calculation
838
+
839
+ ```typescript
840
+ import { calculateCost, getPiModel } from 'noosphere';
841
+
722
842
  const model = getPiModel('openai', 'gpt-4o');
723
843
  const usage = { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0 };
724
844
  const cost = calculateCost(model, usage);
725
- console.log(cost.total); // $0.00625
845
+ console.log(cost.total); // $0.00625
846
+ console.log(cost.input); // $0.0025
847
+ console.log(cost.output); // $0.00375
848
+ ```
849
+
850
+ ### Direct Stream/Complete APIs
851
+
852
+ ```typescript
853
+ import { piComplete, piStream, piCompleteSimple, setApiKey, getPiModel } from 'noosphere';
854
+
855
+ setApiKey('openai', process.env.OPENAI_API_KEY!);
856
+ const model = getPiModel('openai', 'gpt-4o');
857
+
858
+ // Simple completion
859
+ const result = await piCompleteSimple(model, {
860
+ systemPrompt: 'You are helpful.',
861
+ messages: [{ role: 'user', content: 'Hello!', timestamp: Date.now() }],
862
+ });
863
+
864
+ // Streaming
865
+ const stream = piStream(model, {
866
+ messages: [{ role: 'user', content: 'Hello!', timestamp: Date.now() }],
867
+ });
868
+
869
+ for await (const event of stream) {
870
+ if (event.type === 'text_delta') process.stdout.write(event.delta);
871
+ }
726
872
  ```
727
873
 
728
874
  ---
package/dist/index.cjs CHANGED
@@ -2716,6 +2716,36 @@ var OpenAIMediaProvider = class {
2716
2716
  modalities = ["image", "video", "tts", "stt"];
2717
2717
  isLocal = false;
2718
2718
  modelsCache = null;
2719
+ voicesCache = null;
2720
+ /** Auto-fetch available TTS voices by sending an invalid voice and parsing the error. */
2721
+ async fetchVoices() {
2722
+ if (this.voicesCache) return this.voicesCache;
2723
+ try {
2724
+ const res = await fetch(`${OPENAI_API_BASE}/audio/speech`, {
2725
+ method: "POST",
2726
+ headers: {
2727
+ "Content-Type": "application/json",
2728
+ Authorization: `Bearer ${this.apiKey}`
2729
+ },
2730
+ body: JSON.stringify({ model: "tts-1", input: ".", voice: "__discover_voices__" })
2731
+ });
2732
+ if (!res.ok) {
2733
+ const data = await res.json();
2734
+ const msg = data?.error?.message ?? "";
2735
+ const shouldBe = msg.match(/Input should be ([^"]+)/);
2736
+ if (shouldBe) {
2737
+ const voiceList = shouldBe[1].match(/'([a-z]+)'/g);
2738
+ if (voiceList && voiceList.length > 0) {
2739
+ this.voicesCache = voiceList.map((v) => v.replace(/'/g, ""));
2740
+ return this.voicesCache;
2741
+ }
2742
+ }
2743
+ }
2744
+ } catch {
2745
+ }
2746
+ this.voicesCache = [];
2747
+ return this.voicesCache;
2748
+ }
2719
2749
  async ping() {
2720
2750
  try {
2721
2751
  const controller = new AbortController();
@@ -2751,6 +2781,7 @@ var OpenAIMediaProvider = class {
2751
2781
  } finally {
2752
2782
  clearTimeout(timer);
2753
2783
  }
2784
+ const voices = await this.fetchVoices();
2754
2785
  const entries = data?.data ?? [];
2755
2786
  const logo = getProviderLogo("openai");
2756
2787
  const models = [];
@@ -2766,7 +2797,7 @@ var OpenAIMediaProvider = class {
2766
2797
  cost: { price: 0, unit: "per_request" },
2767
2798
  logo,
2768
2799
  description: entry.description,
2769
- capabilities: this.getCapabilities(entry.id, mod)
2800
+ capabilities: this.getCapabilities(entry.id, mod, voices)
2770
2801
  };
2771
2802
  models.push(info);
2772
2803
  }
@@ -2911,7 +2942,7 @@ var OpenAIMediaProvider = class {
2911
2942
  }
2912
2943
  };
2913
2944
  }
2914
- getCapabilities(id, modality) {
2945
+ getCapabilities(id, modality, voices) {
2915
2946
  if (modality === "image") {
2916
2947
  return {
2917
2948
  maxWidth: id.startsWith("dall-e-3") ? 1792 : 1024,
@@ -2920,7 +2951,7 @@ var OpenAIMediaProvider = class {
2920
2951
  }
2921
2952
  if (modality === "tts") {
2922
2953
  return {
2923
- voices: ["alloy", "ash", "coral", "echo", "fable", "onyx", "nova", "sage", "shimmer"]
2954
+ voices: voices && voices.length > 0 ? voices : void 0
2924
2955
  };
2925
2956
  }
2926
2957
  if (modality === "video") {
@@ -2941,18 +2972,34 @@ var OpenAIMediaProvider = class {
2941
2972
  // src/providers/google-media.ts
2942
2973
  var GOOGLE_API_BASE = "https://generativelanguage.googleapis.com/v1beta";
2943
2974
  var FETCH_TIMEOUT_MS6 = 8e3;
2944
- var GOOGLE_TTS_VOICES = [
2945
- "Aoede",
2946
- "Charon",
2947
- "Fenrir",
2948
- "Kore",
2949
- "Puck",
2950
- "Leda",
2951
- "Orus",
2952
- "Perseus",
2953
- "Zephyr",
2954
- "Callirrhoe"
2955
- ];
2975
+ async function fetchGoogleVoices(apiKey) {
2976
+ try {
2977
+ const res = await fetch(
2978
+ `${GOOGLE_API_BASE}/models/gemini-2.5-flash-preview-tts:generateContent?key=${apiKey}`,
2979
+ {
2980
+ method: "POST",
2981
+ headers: { "Content-Type": "application/json" },
2982
+ body: JSON.stringify({
2983
+ contents: [{ parts: [{ text: "." }] }],
2984
+ generationConfig: {
2985
+ response_modalities: ["AUDIO"],
2986
+ speech_config: { voiceConfig: { prebuiltVoiceConfig: { voiceName: "__discover_voices__" } } }
2987
+ }
2988
+ })
2989
+ }
2990
+ );
2991
+ if (!res.ok) {
2992
+ const data = await res.json();
2993
+ const msg = data?.error?.message ?? "";
2994
+ const match = msg.match(/Allowed voice names are:\s*(.+)/i);
2995
+ if (match) {
2996
+ return match[1].split(",").map((v) => v.trim()).filter(Boolean);
2997
+ }
2998
+ }
2999
+ } catch {
3000
+ }
3001
+ return [];
3002
+ }
2956
3003
  function classifyGoogleModel(model) {
2957
3004
  const name = (model.name ?? "").replace("models/", "");
2958
3005
  const methods = model.supportedGenerationMethods ?? [];
@@ -2970,6 +3017,7 @@ var GoogleMediaProvider = class {
2970
3017
  modalities = ["image", "video", "tts"];
2971
3018
  isLocal = false;
2972
3019
  modelsCache = null;
3020
+ voicesCache = null;
2973
3021
  async ping() {
2974
3022
  try {
2975
3023
  const controller = new AbortController();
@@ -3004,6 +3052,9 @@ var GoogleMediaProvider = class {
3004
3052
  clearTimeout(timer);
3005
3053
  }
3006
3054
  const entries = data?.models ?? [];
3055
+ if (!this.voicesCache) {
3056
+ this.voicesCache = await fetchGoogleVoices(this.apiKey);
3057
+ }
3007
3058
  const logo = getProviderLogo("google");
3008
3059
  const models = [];
3009
3060
  for (const entry of entries) {
@@ -3020,7 +3071,7 @@ var GoogleMediaProvider = class {
3020
3071
  cost: { price: 0, unit: modality2 === "video" ? "per_video" : "per_image" },
3021
3072
  logo,
3022
3073
  description: entry.description,
3023
- capabilities: modality2 === "video" ? { maxDuration: 8, supportsStreaming: false } : modality2 === "tts" ? { voices: GOOGLE_TTS_VOICES } : void 0
3074
+ capabilities: modality2 === "video" ? { maxDuration: 8, supportsStreaming: false } : modality2 === "tts" ? { voices: this.voicesCache && this.voicesCache.length > 0 ? this.voicesCache : void 0 } : void 0
3024
3075
  };
3025
3076
  models.push(info);
3026
3077
  }