@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/README.md +3 -3
- package/dist/index.d.mts +26 -13
- package/dist/index.d.ts +26 -13
- package/dist/index.js +64 -31
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +62 -30
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
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
|
|
1069
|
-
//
|
|
1070
|
-
//
|
|
1071
|
-
//
|
|
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
|
-
|
|
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
|
-
//
|
|
1768
|
-
//
|
|
1769
|
-
[VOICE_MODEL_CONSTANTS.GOOGLE_TTS]: { textInputPricePerM:
|
|
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
|
-
|
|
1882
|
+
var WAV_HEADER_BYTES = 44;
|
|
1883
|
+
function normalizeTtsStyle(voiceStyle) {
|
|
1877
1884
|
const style = voiceStyle?.trim().replace(/[:.!,;\s]+$/, "");
|
|
1878
|
-
|
|
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
|
|
1940
|
+
const request = (style2) => client.models.generateContent({
|
|
1928
1941
|
model: VOICE_MODEL_CONSTANTS.GOOGLE_TTS,
|
|
1929
|
-
contents: [{ parts: [{ text:
|
|
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
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
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(
|
|
1946
|
-
return {
|
|
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,
|