@hiper2d/ai-agents 0.14.1 → 0.16.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.mjs CHANGED
@@ -1065,10 +1065,15 @@ var SupportedAiModels = {
1065
1065
  // Live 2026-08-30 (one sample each): day-2 vote 11.8s, 15-character story 56.2s.
1066
1066
  tags: ["cheap"]
1067
1067
  },
1068
- // Sakana Fugu models — OpenAI-compatible. They reason internally (and bill it as
1069
- // "orchestration" tokens), but never surface reasoning to us: responses come back with
1070
- // reasoning_tokens: 0 and no reasoning_content. So hasThinking is false — there's no
1071
- // thinking content to show and no user-facing thinking toggle. Single entry per model.
1068
+ // Sakana Fugu models — OpenAI-compatible. They reason internally, but Chat Completions
1069
+ // (what FuguAgent uses) never returns the text: reasoning_tokens are reported and billed,
1070
+ // there is no reasoning_content. So hasThinking is false. Single entry per model.
1071
+ // The Messages and Responses APIs DO return reasoning (thinking blocks with an empty
1072
+ // signature / reasoning items, some with encrypted_content), but probed 2026-09-24 on
1073
+ // fugu-max it isn't worth switching for, since we keep reasoning only to replay it: the
1074
+ // returned text is often a vague status summary or empty rather than the actual
1075
+ // reasoning, and replayed reasoning reaches the model distorted (a planted "4817" came
1076
+ // back as "4829" on both APIs, presumably rewritten by the orchestrator).
1072
1077
  //
1073
1078
  // Base `fugu` was RETIRED 2026-08-04. It was carried as a cheap everyday option at an
1074
1079
  // assumed $1/$3, but reconciling token logs against the Sakana balance showed it actually
@@ -1751,8 +1756,9 @@ function extractTokenUsageFromResponse8(response) {
1751
1756
  var VOICE_MODEL_CONSTANTS = {
1752
1757
  OPENAI_TTS: "gpt-4o-mini-tts",
1753
1758
  OPENAI_STT: "whisper-1",
1754
- // ai.google.dev/gemini-api/docs/speech-generation
1755
- GOOGLE_TTS: "gemini-3.1-flash-tts-preview",
1759
+ // ai.google.dev/gemini-api/docs/speech-generation — Google's named replacement
1760
+ // for gemini-3.1-flash-tts-preview (2026-09-24: ~6 s vs ~15 s per line, ~4x cheaper)
1761
+ GOOGLE_TTS: "gemini-3.8-flash-lite-tts",
1756
1762
  // ai.google.dev/gemini-api/docs/transcribe — Interactions API only (see google-stt.ts)
1757
1763
  GOOGLE_STT: "gemini-3.5-transcribe"
1758
1764
  };
@@ -1764,9 +1770,9 @@ var VOICE_PROVIDER_API_KEY = {
1764
1770
  var VOICE_MODEL_PRICING = {
1765
1771
  [VOICE_MODEL_CONSTANTS.OPENAI_TTS]: { pricePerMillionCharacters: 15 },
1766
1772
  [VOICE_MODEL_CONSTANTS.OPENAI_STT]: { pricePerMinute: 6e-3 },
1767
- // Measured 2026-09-05: ~32 audio tokens per second of speech, so a 15-second
1768
- // line is ~$0.01 — about 3-5x an OpenAI line of the same length.
1769
- [VOICE_MODEL_CONSTANTS.GOOGLE_TTS]: { textInputPricePerM: 1, audioOutputPricePerM: 20 },
1773
+ // ~32 audio tokens per second of speech, so a 15-second line is ~$0.003.
1774
+ // Google doubles both rates to $1 / $12 on 2027-01-01 — update then.
1775
+ [VOICE_MODEL_CONSTANTS.GOOGLE_TTS]: { textInputPricePerM: 0.5, audioOutputPricePerM: 6 },
1770
1776
  // ~25 audio tokens per second in, ~175 text tokens per minute out: ≈ $0.005/min.
1771
1777
  [VOICE_MODEL_CONSTANTS.GOOGLE_STT]: { audioInputPricePerM: 2, textOutputPricePerM: 12 }
1772
1778
  };
@@ -1873,12 +1879,10 @@ import { GoogleGenAI } from "@google/genai";
1873
1879
  var AUDIO_TOKENS_PER_SECOND = 32;
1874
1880
  var SAMPLE_RATE = 24e3;
1875
1881
  var PCM_BYTES_PER_SECOND = SAMPLE_RATE * 2;
1876
- function buildGoogleTtsPrompt(text, voiceStyle) {
1882
+ var WAV_HEADER_BYTES = 44;
1883
+ function normalizeTtsStyle(voiceStyle) {
1877
1884
  const style = voiceStyle?.trim().replace(/[:.!,;\s]+$/, "");
1878
- if (!style) return text;
1879
- const isShort = style.split(/\s+/).length <= 3 && !/[.!?,;]/.test(style);
1880
- return isShort ? `Say ${style}: ${text}` : `${style}:
1881
- ${text}`;
1885
+ return style || void 0;
1882
1886
  }
1883
1887
  function pcmToWav(pcmData, sampleRate = SAMPLE_RATE) {
1884
1888
  const numChannels = 1;
@@ -1922,28 +1926,51 @@ function describeEmptyTtsResponse(response) {
1922
1926
  if (!parts.length) bits.push("no parts");
1923
1927
  return bits.join(", ");
1924
1928
  }
1929
+ function isSafetyBlocked(response) {
1930
+ const finish = response?.candidates?.[0]?.finishReason;
1931
+ return finish === "SAFETY" || finish === "PROHIBITED_CONTENT" || !!response?.promptFeedback?.blockReason;
1932
+ }
1933
+ function findAudioPart(response) {
1934
+ const parts = response?.candidates?.[0]?.content?.parts ?? [];
1935
+ const inline = parts.find((part) => part.inlineData?.mimeType?.startsWith("audio/") && part.inlineData?.data)?.inlineData;
1936
+ return inline ? { data: inline.data, mimeType: inline.mimeType } : void 0;
1937
+ }
1925
1938
  async function generateGoogleTtsAudio(text, apiKey, options) {
1926
1939
  const client = new GoogleGenAI({ apiKey });
1927
- const response = await client.models.generateContent({
1940
+ const request = (style2) => client.models.generateContent({
1928
1941
  model: VOICE_MODEL_CONSTANTS.GOOGLE_TTS,
1929
- contents: [{ parts: [{ text: buildGoogleTtsPrompt(text, options.voiceStyle) }] }],
1942
+ contents: [{ parts: [style2 ? { text, speechMetadata: { style: style2 } } : { text }] }],
1930
1943
  config: {
1931
1944
  responseModalities: ["AUDIO"],
1932
1945
  speechConfig: { voiceConfig: { prebuiltVoiceConfig: { voiceName: options.voiceName } } }
1933
1946
  }
1934
1947
  });
1935
- const candidate = response.candidates?.[0];
1936
- const parts = candidate?.content?.parts ?? [];
1937
- const audioPart = parts.find((part) => part.inlineData?.mimeType?.startsWith("audio/"));
1938
- if (!audioPart?.inlineData?.data) {
1939
- throw new Error(`No audio data in Google TTS response (${describeEmptyTtsResponse(response)})`);
1940
- }
1941
- const pcmData = new Uint8Array(Buffer.from(audioPart.inlineData.data, "base64"));
1948
+ const style = normalizeTtsStyle(options.voiceStyle);
1949
+ let response = await request(style);
1950
+ let styleDropped = false;
1951
+ let blockedInputTokens = 0;
1952
+ if (!findAudioPart(response) && style && isSafetyBlocked(response)) {
1953
+ blockedInputTokens = response?.usageMetadata?.promptTokenCount ?? 0;
1954
+ response = await request(void 0);
1955
+ styleDropped = true;
1956
+ }
1957
+ const audioPart = findAudioPart(response);
1958
+ if (!audioPart) {
1959
+ throw new Error(`No audio data in Google TTS response (${describeEmptyTtsResponse(response)}${styleDropped ? ", also without the style" : ""})`);
1960
+ }
1961
+ const bytes = new Uint8Array(Buffer.from(audioPart.data, "base64"));
1962
+ const isWav = /^audio\/(x-)?wav/i.test(audioPart.mimeType);
1963
+ const audio = isWav ? bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) : pcmToWav(bytes);
1964
+ const pcmBytes = isWav ? Math.max(0, bytes.length - WAV_HEADER_BYTES) : bytes.length;
1942
1965
  const usageMetadata = response.usageMetadata ?? {};
1943
- const inputTokens = usageMetadata.promptTokenCount ?? 0;
1966
+ const inputTokens = (usageMetadata.promptTokenCount ?? 0) + blockedInputTokens;
1944
1967
  const reportedOutput = usageMetadata.candidatesTokenCount;
1945
- const outputTokens = reportedOutput && reportedOutput > 0 ? reportedOutput : Math.ceil(pcmData.length / PCM_BYTES_PER_SECOND * AUDIO_TOKENS_PER_SECOND);
1946
- return { audio: pcmToWav(pcmData), usage: { inputTokens, outputTokens } };
1968
+ const outputTokens = reportedOutput && reportedOutput > 0 ? reportedOutput : Math.ceil(pcmBytes / PCM_BYTES_PER_SECOND * AUDIO_TOKENS_PER_SECOND);
1969
+ return {
1970
+ audio,
1971
+ usage: { inputTokens, outputTokens },
1972
+ ...styleDropped ? { styleDropped } : {}
1973
+ };
1947
1974
  }
1948
1975
 
1949
1976
  // src/voice/google-stt.ts
@@ -1974,11 +2001,11 @@ var GoogleVoiceAgent = class {
1974
2001
  ttsModel = VOICE_MODEL_CONSTANTS.GOOGLE_TTS;
1975
2002
  sttModel = VOICE_MODEL_CONSTANTS.GOOGLE_STT;
1976
2003
  async speak(request) {
1977
- const { audio, usage } = await generateGoogleTtsAudio(request.text, this.apiKey, {
2004
+ const { audio, usage, styleDropped } = await generateGoogleTtsAudio(request.text, this.apiKey, {
1978
2005
  voiceName: request.voice,
1979
2006
  voiceStyle: request.voiceStyle
1980
2007
  });
1981
- return { audio, costUSD: calculateGeminiTtsCost(usage), usage };
2008
+ return { audio, costUSD: calculateGeminiTtsCost(usage), usage, ...styleDropped ? { styleDropped } : {} };
1982
2009
  }
1983
2010
  async transcribe(request) {
1984
2011
  const { text, durationSeconds, usage } = await transcribeWithGemini(request.audio, this.apiKey, { mimeType: request.mimeType });
@@ -4734,7 +4761,11 @@ ${schemaDescription}`;
4734
4761
  const params = {
4735
4762
  ...this.defaultParams,
4736
4763
  messages: openAIMessages,
4737
- ...this.thinkingParams()
4764
+ ...this.thinkingParams(),
4765
+ response_format: {
4766
+ type: "json_schema",
4767
+ json_schema: ZodSchemaConverter.toOpenAIJsonSchema(zodSchema, "response_schema")
4768
+ }
4738
4769
  };
4739
4770
  completion = await this.client.chat.completions.create(params);
4740
4771
  } catch (apiError) {
@@ -5342,7 +5373,6 @@ export {
5342
5373
  XAI_REASONING_EFFORTS,
5343
5374
  ZodSchemaConverter,
5344
5375
  applySpend,
5345
- buildGoogleTtsPrompt,
5346
5376
  calculateAnthropicCost,
5347
5377
  calculateCost,
5348
5378
  calculateDeepSeekCost,
@@ -5395,6 +5425,7 @@ export {
5395
5425
  isHybridThinkingModel,
5396
5426
  isInPeakWindow,
5397
5427
  isPeakBilling,
5428
+ isSafetyBlocked,
5398
5429
  isWeekendAt,
5399
5430
  ledgerSpend,
5400
5431
  logger,
@@ -5402,6 +5433,7 @@ export {
5402
5433
  modelHasTag,
5403
5434
  modelIsFast,
5404
5435
  needsPromptBasedSchema,
5436
+ normalizeTtsStyle,
5405
5437
  parseAndValidateLlmJson,
5406
5438
  pcmToWav,
5407
5439
  periodEnd,