noosphere 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,6 +16,8 @@ One import. Every model. Every modality.
16
16
  - **30+ HuggingFace tasks** — LLM, image, TTS, translation, summarization, classification, and more
17
17
  - **Local-first architecture** — Auto-detects Ollama, ComfyUI, Whisper, AudioCraft, Piper, and Kokoro on your machine
18
18
  - **Org-aware logos** — HuggingFace models show the real org logo (Meta, Google, NVIDIA) instead of generic HF logo
19
+ - **Pre-request token counting** — Count tokens before sending, for ALL providers (OpenAI/Groq/Ollama via tiktoken, Google/Anthropic via API)
20
+ - **Full pi-ai access** — Agent loop with tool calling, preprocessor (compaction hook), `calculateCost`, direct stream/complete APIs — all re-exported
19
21
  - **Agentic capabilities** — Tool use, function calling, reasoning/thinking, vision, and agent loops via Pi-AI
20
22
  - **Failover & retry** — Automatic retries with exponential backoff and cross-provider failover
21
23
  - **Usage tracking** — Real-time cost, latency, and token tracking across all providers
@@ -629,6 +631,102 @@ Noosphere auto-detects all local runtimes on startup:
629
631
 
630
632
  ---
631
633
 
634
+ ## Pre-Request Token Counting
635
+
636
+ Count tokens **before** sending a request to any provider. Know the cost upfront.
637
+
638
+ ```typescript
639
+ // Via Noosphere instance (auto-routes by model)
640
+ const result = await ai.countTokens({
641
+ messages: [
642
+ { role: 'system', content: 'You are a helpful assistant.' },
643
+ { role: 'user', content: 'Explain quantum computing.' },
644
+ ],
645
+ model: 'gpt-4o',
646
+ });
647
+ console.log(result.tokens); // 26
648
+ console.log(result.method); // "tiktoken" (instant, local)
649
+ console.log(result.provider); // "openai"
650
+
651
+ // Google — exact count via API
652
+ const google = await ai.countTokens({
653
+ messages: [{ role: 'user', content: 'Hello!' }],
654
+ model: 'gemini-2.5-flash',
655
+ });
656
+ console.log(google.tokens); // 3
657
+ console.log(google.method); // "api" (exact)
658
+ ```
659
+
660
+ **Token counting by provider:**
661
+
662
+ | Provider | Method | Speed | Accuracy |
663
+ |---|---|---|---|
664
+ | **OpenAI** (GPT-4o, o1, o3, o4, GPT-5) | tiktoken (local) | Instant | Exact |
665
+ | **Google** (Gemini) | `/countTokens` API | ~200ms | Exact |
666
+ | **Anthropic** (Claude) | `/messages/count_tokens` API | ~200ms | Exact |
667
+ | **Groq** (Llama, Mixtral, Gemma) | tiktoken (local) | Instant | Exact |
668
+ | **Cerebras** (Llama) | tiktoken (local) | Instant | Exact |
669
+ | **Mistral** (Mistral, Mixtral, Codestral) | tiktoken (local) | Instant | Close approx |
670
+ | **xAI** (Grok) | tiktoken (local) | Instant | Close approx |
671
+ | **OpenRouter** (all models) | tiktoken (local) | Instant | Close approx |
672
+ | **Ollama** (all local models) | tiktoken (local) | Instant | Close approx |
673
+
674
+ You can also use standalone functions without a Noosphere instance:
675
+
676
+ ```typescript
677
+ import {
678
+ countTokensOpenAI, countTokensGoogle, countTokensAnthropic,
679
+ countTokensGroq, countTokensMistral, countTokensXai,
680
+ countTokensCerebras, countTokensOpenRouter, countTokensOllama,
681
+ } from 'noosphere';
682
+
683
+ // Local (instant, no API key needed)
684
+ const tokens = countTokensOpenAI(messages, 'gpt-4o'); // 26
685
+ const groq = countTokensGroq(messages, 'llama-3.3-70b'); // 26
686
+ const ollama = countTokensOllama(messages, 'qwen3:8b'); // 26
687
+
688
+ // API-based (exact, needs key)
689
+ const google = await countTokensGoogle(messages, GEMINI_KEY, 'gemini-2.5-flash'); // 16
690
+ const claude = await countTokensAnthropic(messages, ANTHROPIC_KEY, 'claude-sonnet-4-20250514'); // exact
691
+ ```
692
+
693
+ ---
694
+
695
+ ## Agent Loop & pi-ai Access
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.
698
+
699
+ ```typescript
700
+ import {
701
+ agentLoop, calculateCost,
702
+ piStream, piComplete, piStreamSimple, piCompleteSimple,
703
+ setApiKey, getApiKey, getPiModel, getPiModels, getPiProviders,
704
+ } from 'noosphere';
705
+
706
+ // Agent loop with tool calling and preprocessor (compaction hook)
707
+ import type { AgentLoopConfig, AgentContext, AgentTool } from 'noosphere';
708
+
709
+ const config: AgentLoopConfig = {
710
+ model: getPiModel('openai', 'gpt-4o'),
711
+ // Preprocessor runs before each LLM call — use for context compaction
712
+ preprocessor: async (messages) => {
713
+ // Truncate old messages, summarize, etc.
714
+ if (messages.length > 50) {
715
+ return messages.slice(-20); // keep last 20
716
+ }
717
+ return messages;
718
+ },
719
+ };
720
+
721
+ // Calculate cost before sending
722
+ const model = getPiModel('openai', 'gpt-4o');
723
+ const usage = { input: 1000, output: 500, cacheRead: 0, cacheWrite: 0 };
724
+ const cost = calculateCost(model, usage);
725
+ console.log(cost.total); // $0.00625
726
+ ```
727
+
728
+ ---
729
+
632
730
  ## Configuration
633
731
 
634
732
  API keys are resolved from the constructor config or environment variables (config takes priority):
package/dist/index.cjs CHANGED
@@ -31,9 +31,30 @@ __export(index_exports, {
31
31
  PROVIDER_IDS: () => PROVIDER_IDS,
32
32
  PROVIDER_LOGOS: () => PROVIDER_LOGOS,
33
33
  WhisperLocalProvider: () => WhisperLocalProvider,
34
+ agentLoop: () => import_pi_ai3.agentLoop,
35
+ calculateCost: () => import_pi_ai4.calculateCost,
36
+ countTokens: () => countTokens,
37
+ countTokensAnthropic: () => countTokensAnthropic,
38
+ countTokensCerebras: () => countTokensCerebras,
39
+ countTokensGoogle: () => countTokensGoogle,
40
+ countTokensGroq: () => countTokensGroq,
41
+ countTokensMistral: () => countTokensMistral,
42
+ countTokensOllama: () => countTokensOllama,
43
+ countTokensOpenAI: () => countTokensOpenAI,
44
+ countTokensOpenRouter: () => countTokensOpenRouter,
45
+ countTokensXai: () => countTokensXai,
34
46
  detectOpenAICompatServers: () => detectOpenAICompatServers,
35
47
  getAllProviderLogos: () => getAllProviderLogos,
36
- getProviderLogo: () => getProviderLogo
48
+ getApiKey: () => import_pi_ai6.getApiKey,
49
+ getPiModel: () => import_pi_ai4.getModel,
50
+ getPiModels: () => import_pi_ai4.getModels,
51
+ getPiProviders: () => import_pi_ai4.getProviders,
52
+ getProviderLogo: () => getProviderLogo,
53
+ piComplete: () => import_pi_ai5.complete,
54
+ piCompleteSimple: () => import_pi_ai5.completeSimple,
55
+ piStream: () => import_pi_ai5.stream,
56
+ piStreamSimple: () => import_pi_ai5.streamSimple,
57
+ setApiKey: () => import_pi_ai6.setApiKey
37
58
  });
38
59
  module.exports = __toCommonJS(index_exports);
39
60
 
@@ -140,6 +161,136 @@ function resolveConfig(input) {
140
161
  };
141
162
  }
142
163
 
164
+ // src/token-counter.ts
165
+ var import_tiktoken = require("tiktoken");
166
+ var TIKTOKEN_MODEL_MAP = {
167
+ "gpt-4o": "gpt-4o",
168
+ "gpt-4o-mini": "gpt-4o-mini",
169
+ "gpt-4-turbo": "gpt-4-turbo",
170
+ "gpt-4": "gpt-4",
171
+ "gpt-3.5-turbo": "gpt-3.5-turbo"
172
+ };
173
+ function resolveTiktokenModel(model) {
174
+ if (model in TIKTOKEN_MODEL_MAP) return TIKTOKEN_MODEL_MAP[model];
175
+ for (const [prefix, tikModel] of Object.entries(TIKTOKEN_MODEL_MAP)) {
176
+ if (model.startsWith(prefix)) return tikModel;
177
+ }
178
+ return "gpt-4o";
179
+ }
180
+ function countWithTiktoken(messages, model) {
181
+ const tikModel = resolveTiktokenModel(model);
182
+ const enc = (0, import_tiktoken.encoding_for_model)(tikModel);
183
+ let tokens = 0;
184
+ for (const msg of messages) {
185
+ tokens += 4;
186
+ tokens += enc.encode(msg.role).length;
187
+ tokens += enc.encode(msg.content).length;
188
+ }
189
+ tokens += 2;
190
+ enc.free();
191
+ return tokens;
192
+ }
193
+ function countTokensOpenAI(messages, model = "gpt-4o") {
194
+ return countWithTiktoken(messages, model);
195
+ }
196
+ async function countTokensGoogle(messages, apiKey, model = "gemini-2.5-flash") {
197
+ const contents = messages.map((m) => ({
198
+ role: m.role === "assistant" ? "model" : "user",
199
+ parts: [{ text: m.content }]
200
+ }));
201
+ const res = await fetch(
202
+ `https://generativelanguage.googleapis.com/v1beta/models/${model}:countTokens?key=${apiKey}`,
203
+ {
204
+ method: "POST",
205
+ headers: { "Content-Type": "application/json" },
206
+ body: JSON.stringify({ contents })
207
+ }
208
+ );
209
+ if (!res.ok) {
210
+ throw new Error(`Google countTokens failed (${res.status}): ${await res.text()}`);
211
+ }
212
+ const data = await res.json();
213
+ return data.totalTokens;
214
+ }
215
+ async function countTokensAnthropic(messages, apiKey, model = "claude-sonnet-4-20250514") {
216
+ const anthropicMessages = messages.filter((m) => m.role !== "system").map((m) => ({ role: m.role, content: m.content }));
217
+ const systemPrompt = messages.find((m) => m.role === "system")?.content;
218
+ const body = {
219
+ model,
220
+ messages: anthropicMessages
221
+ };
222
+ if (systemPrompt) body.system = systemPrompt;
223
+ const res = await fetch("https://api.anthropic.com/v1/messages/count_tokens", {
224
+ method: "POST",
225
+ headers: {
226
+ "x-api-key": apiKey,
227
+ "anthropic-version": "2023-06-01",
228
+ "Content-Type": "application/json"
229
+ },
230
+ body: JSON.stringify(body)
231
+ });
232
+ if (!res.ok) {
233
+ throw new Error(`Anthropic countTokens failed (${res.status}): ${await res.text()}`);
234
+ }
235
+ const data = await res.json();
236
+ return data.input_tokens;
237
+ }
238
+ function countTokensGroq(messages, model = "llama-3.3-70b-versatile") {
239
+ return countWithTiktoken(messages, model);
240
+ }
241
+ function countTokensMistral(messages, model = "mistral-large-latest") {
242
+ return countWithTiktoken(messages, model);
243
+ }
244
+ function countTokensXai(messages, model = "grok-3") {
245
+ return countWithTiktoken(messages, model);
246
+ }
247
+ function countTokensCerebras(messages, model = "llama-3.3-70b") {
248
+ return countWithTiktoken(messages, model);
249
+ }
250
+ function countTokensOpenRouter(messages, model = "openai/gpt-4o") {
251
+ return countWithTiktoken(messages, model);
252
+ }
253
+ function countTokensOllama(messages, model = "llama3.2") {
254
+ return countWithTiktoken(messages, model);
255
+ }
256
+ var PROVIDER_MODEL_PREFIXES = [
257
+ { prefixes: ["gemini", "imagen", "veo"], provider: "google" },
258
+ { prefixes: ["claude"], provider: "anthropic" },
259
+ { prefixes: ["gpt-", "o1", "o3", "o4", "chatgpt", "dall-e", "gpt-image", "tts-", "whisper", "sora"], provider: "openai" },
260
+ { prefixes: ["grok"], provider: "xai" },
261
+ { prefixes: ["mistral", "mixtral", "codestral", "ministral"], provider: "mistral" },
262
+ { prefixes: ["llama", "gemma", "qwen", "deepseek", "phi"], provider: "groq" }
263
+ ];
264
+ function inferProvider(model) {
265
+ const lower = model.toLowerCase();
266
+ for (const { prefixes, provider } of PROVIDER_MODEL_PREFIXES) {
267
+ for (const prefix of prefixes) {
268
+ if (lower.startsWith(prefix)) return provider;
269
+ }
270
+ }
271
+ return "unknown";
272
+ }
273
+ async function countTokens(options, apiKeys) {
274
+ const model = options.model ?? "gpt-4o";
275
+ const provider = options.provider ?? inferProvider(model);
276
+ if (provider === "google" && apiKeys?.google) {
277
+ const tokens2 = await countTokensGoogle(options.messages, apiKeys.google, model);
278
+ return { tokens: tokens2, model, provider: "google", method: "api" };
279
+ }
280
+ if (provider === "anthropic" && apiKeys?.anthropic) {
281
+ const tokens2 = await countTokensAnthropic(options.messages, apiKeys.anthropic, model);
282
+ return { tokens: tokens2, model, provider: "anthropic", method: "api" };
283
+ }
284
+ const tokens = countWithTiktoken(options.messages, model);
285
+ const resolvedProvider = provider === "unknown" ? "openai" : provider;
286
+ return {
287
+ tokens,
288
+ model,
289
+ provider: resolvedProvider,
290
+ method: provider === "openai" ? "tiktoken" : "tiktoken"
291
+ };
292
+ }
293
+
143
294
  // src/logos.ts
144
295
  var CDN_BASE = "https://blockchainstarter.nyc3.digitaloceanspaces.com/noosphere/logos";
145
296
  var PROVIDER_IDS = [
@@ -2972,7 +3123,7 @@ var GoogleMediaProvider = class {
2972
3123
  };
2973
3124
  }
2974
3125
  async video(options) {
2975
- const model = options.model ?? "veo-3.0-generate-001";
3126
+ const model = options.model ?? "veo-2.0-generate-001";
2976
3127
  const start = Date.now();
2977
3128
  const body = {
2978
3129
  instances: [{ prompt: options.prompt }],
@@ -3007,10 +3158,15 @@ var GoogleMediaProvider = class {
3007
3158
  if (!pollRes.ok) continue;
3008
3159
  const status = await pollRes.json();
3009
3160
  if (status.done) {
3010
- const videoBase64 = status.response?.generatedSamples?.[0]?.video?.bytesBase64Encoded;
3011
- if (videoBase64) {
3161
+ if (status.error) {
3162
+ throw new Error(`Google video generation error: ${status.error.message ?? JSON.stringify(status.error)}`);
3163
+ }
3164
+ const resp = status.response ?? {};
3165
+ const samples = resp.generateVideoResponse?.generatedSamples ?? resp.generatedSamples ?? [];
3166
+ const video = samples[0]?.video;
3167
+ if (video?.bytesBase64Encoded) {
3012
3168
  return {
3013
- buffer: Buffer.from(videoBase64, "base64"),
3169
+ buffer: Buffer.from(video.bytesBase64Encoded, "base64"),
3014
3170
  provider: "google-media",
3015
3171
  model,
3016
3172
  modality: "video",
@@ -3019,16 +3175,32 @@ var GoogleMediaProvider = class {
3019
3175
  media: { format: "mp4", duration: options.duration }
3020
3176
  };
3021
3177
  }
3022
- const videoUrl = status.response?.generatedSamples?.[0]?.video?.uri;
3023
- return {
3024
- url: videoUrl,
3025
- provider: "google-media",
3026
- model,
3027
- modality: "video",
3028
- latencyMs: Date.now() - start,
3029
- usage: { cost: 0, unit: "per_video" },
3030
- media: { format: "mp4", duration: options.duration }
3031
- };
3178
+ if (video?.uri) {
3179
+ const separator = video.uri.includes("?") ? "&" : "?";
3180
+ const videoRes = await fetch(`${video.uri}${separator}key=${this.apiKey}`, { redirect: "follow" });
3181
+ if (videoRes.ok) {
3182
+ const buffer = Buffer.from(await videoRes.arrayBuffer());
3183
+ return {
3184
+ buffer,
3185
+ provider: "google-media",
3186
+ model,
3187
+ modality: "video",
3188
+ latencyMs: Date.now() - start,
3189
+ usage: { cost: 0, unit: "per_video" },
3190
+ media: { format: "mp4", duration: options.duration }
3191
+ };
3192
+ }
3193
+ return {
3194
+ url: video.uri,
3195
+ provider: "google-media",
3196
+ model,
3197
+ modality: "video",
3198
+ latencyMs: Date.now() - start,
3199
+ usage: { cost: 0, unit: "per_video" },
3200
+ media: { format: "mp4", duration: options.duration }
3201
+ };
3202
+ }
3203
+ throw new Error("Google video generation completed but returned no video data");
3032
3204
  }
3033
3205
  }
3034
3206
  throw new Error(`Google video generation timed out after 5 minutes`);
@@ -3225,6 +3397,12 @@ var Noosphere = class {
3225
3397
  getUsage(options) {
3226
3398
  return this.tracker.getSummary(options);
3227
3399
  }
3400
+ async countTokens(options) {
3401
+ const keys = {};
3402
+ if (this.config.keys?.google) keys.google = this.config.keys.google;
3403
+ if (this.config.keys?.anthropic) keys.anthropic = this.config.keys.anthropic;
3404
+ return countTokens(options, keys);
3405
+ }
3228
3406
  // --- Local Model Management ---
3229
3407
  async installModel(name) {
3230
3408
  if (!this.initialized) await this.init();
@@ -3459,6 +3637,12 @@ var Noosphere = class {
3459
3637
  await this.tracker.record(event);
3460
3638
  }
3461
3639
  };
3640
+
3641
+ // src/index.ts
3642
+ var import_pi_ai3 = require("@mariozechner/pi-ai");
3643
+ var import_pi_ai4 = require("@mariozechner/pi-ai");
3644
+ var import_pi_ai5 = require("@mariozechner/pi-ai");
3645
+ var import_pi_ai6 = require("@mariozechner/pi-ai");
3462
3646
  // Annotate the CommonJS export names for ESM import in node:
3463
3647
  0 && (module.exports = {
3464
3648
  AudioCraftProvider,
@@ -3472,8 +3656,29 @@ var Noosphere = class {
3472
3656
  PROVIDER_IDS,
3473
3657
  PROVIDER_LOGOS,
3474
3658
  WhisperLocalProvider,
3659
+ agentLoop,
3660
+ calculateCost,
3661
+ countTokens,
3662
+ countTokensAnthropic,
3663
+ countTokensCerebras,
3664
+ countTokensGoogle,
3665
+ countTokensGroq,
3666
+ countTokensMistral,
3667
+ countTokensOllama,
3668
+ countTokensOpenAI,
3669
+ countTokensOpenRouter,
3670
+ countTokensXai,
3475
3671
  detectOpenAICompatServers,
3476
3672
  getAllProviderLogos,
3477
- getProviderLogo
3673
+ getApiKey,
3674
+ getPiModel,
3675
+ getPiModels,
3676
+ getPiProviders,
3677
+ getProviderLogo,
3678
+ piComplete,
3679
+ piCompleteSimple,
3680
+ piStream,
3681
+ piStreamSimple,
3682
+ setApiKey
3478
3683
  });
3479
3684
  //# sourceMappingURL=index.cjs.map