From 816a3a856bf9631fe2448ff780ce9ff4b0ad5307 Mon Sep 17 00:00:00 2001 From: Trynax Date: Sat, 18 Oct 2025 16:05:42 +0100 Subject: [PATCH 1/5] Merge upstream changes and reapply audio transcription implementationy --- .../server/src/clients/openai-audio-client.ts | 128 +++++++++ .../src/providers/OpenAIAudioProvider.ts | 89 ++++++ .../server/src/providers/ProviderFactory.ts | 30 ++ .../app/server/src/providers/ProviderType.ts | 1 + .../server/src/services/AccountingService.ts | 6 + .../next/src/app/components/audio.tsx | 258 ++++++++++++++++++ .../src/app/components/tabs-container.tsx | 19 +- packages/sdk/examples/vite/src/App.tsx | 14 +- .../src/components/AudioTranscription.tsx | 258 ++++++++++++++++++ packages/sdk/ts/src/index.ts | 3 + packages/sdk/ts/src/resources/models.ts | 6 + .../ts/src/supported-models/audio/openai.ts | 16 ++ packages/sdk/ts/src/supported-models/index.ts | 1 + packages/sdk/ts/src/supported-models/types.ts | 6 + packages/tests/provider-smoke/.env.example | 3 + .../openai-audio-transcription.test.ts | 175 ++++++++++++ .../provider-smoke/test-audio/sample.wav | Bin 0 -> 45260 bytes pnpm-lock.yaml | 66 +---- ...7-5f18cd68f2f3e65afb6a522ab737b6feeca74632 | 1 + ...7-e05636076a4a4019d7b276202483f8b60f7e7406 | 0 20 files changed, 1022 insertions(+), 58 deletions(-) create mode 100644 packages/app/server/src/clients/openai-audio-client.ts create mode 100644 packages/app/server/src/providers/OpenAIAudioProvider.ts create mode 100644 packages/sdk/examples/next/src/app/components/audio.tsx create mode 100644 packages/sdk/examples/vite/src/components/AudioTranscription.tsx create mode 100644 packages/sdk/ts/src/supported-models/audio/openai.ts create mode 100644 packages/tests/provider-smoke/.env.example create mode 100644 packages/tests/provider-smoke/openai-audio-transcription.test.ts create mode 100644 packages/tests/provider-smoke/test-audio/sample.wav create mode 100644 tsx-0/17607-5f18cd68f2f3e65afb6a522ab737b6feeca74632 create mode 100644 tsx-0/17607-e05636076a4a4019d7b276202483f8b60f7e7406 diff --git a/packages/app/server/src/clients/openai-audio-client.ts b/packages/app/server/src/clients/openai-audio-client.ts new file mode 100644 index 000000000..23e70524e --- /dev/null +++ b/packages/app/server/src/clients/openai-audio-client.ts @@ -0,0 +1,128 @@ +import FormData from 'form-data'; +import { File } from 'buffer'; +import fetch from 'node-fetch'; + +export interface TranscriptionOptions { + model: 'whisper-1' | 'whisper-large-v3'; + language?: string; + prompt?: string; + response_format?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt'; + temperature?: number; +} + +export interface TranslationOptions { + model: 'whisper-1' | 'whisper-large-v3'; + prompt?: string; + response_format?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt'; + temperature?: number; +} + +export interface AudioTranscriptionResponse { + text: string; + task?: string; + language?: string; + duration?: number; + segments?: Array<{ + id: number; + seek: number; + start: number; + end: number; + text: string; + tokens: number[]; + temperature: number; + avg_logprob: number; + compression_ratio: number; + no_speech_prob: number; + }>; +} + +export class OpenAIAudioClient { + private apiKey: string; + private baseUrl: string; + + constructor(apiKey: string, baseUrl = 'https://api.openai.com/v1') { + this.apiKey = apiKey; + this.baseUrl = baseUrl; + } + + /** + * Transcribe audio using OpenAI Whisper API + */ + async transcribe( + audioBuffer: Buffer, + filename: string, + options: TranscriptionOptions + ): Promise { + const formData = new FormData(); + + // Create File from buffer for multipart upload + const file = new File([audioBuffer], filename, { type: 'audio/wav' }); + formData.append('file', file.stream(), { + filename: filename, + contentType: 'audio/wav', + }); + + formData.append('model', options.model); + + if (options.language) formData.append('language', options.language); + if (options.prompt) formData.append('prompt', options.prompt); + if (options.response_format) formData.append('response_format', options.response_format); + if (options.temperature !== undefined) formData.append('temperature', options.temperature.toString()); + + const response = await fetch(`${this.baseUrl}/audio/transcriptions`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + ...formData.getHeaders(), + }, + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`OpenAI Audio API error: ${response.status} - ${errorText}`); + } + + return response.json() as Promise; + } + + /** + * Translate audio to English using OpenAI Whisper API + */ + async translate( + audioBuffer: Buffer, + filename: string, + options: TranslationOptions + ): Promise { + const formData = new FormData(); + + // Create File from buffer for multipart upload + const file = new File([audioBuffer], filename, { type: 'audio/wav' }); + formData.append('file', file.stream(), { + filename: filename, + contentType: 'audio/wav', + }); + + formData.append('model', options.model); + + if (options.prompt) formData.append('prompt', options.prompt); + if (options.response_format) formData.append('response_format', options.response_format); + if (options.temperature !== undefined) formData.append('temperature', options.temperature.toString()); + + const response = await fetch(`${this.baseUrl}/audio/translations`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.apiKey}`, + ...formData.getHeaders(), + }, + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`OpenAI Audio API error: ${response.status} - ${errorText}`); + } + + return response.json() as Promise; + } +} \ No newline at end of file diff --git a/packages/app/server/src/providers/OpenAIAudioProvider.ts b/packages/app/server/src/providers/OpenAIAudioProvider.ts new file mode 100644 index 000000000..6ec4d6cf2 --- /dev/null +++ b/packages/app/server/src/providers/OpenAIAudioProvider.ts @@ -0,0 +1,89 @@ +import { Decimal } from '@prisma/client/runtime/library'; +import { Transaction } from '../types'; +import { BaseProvider } from './BaseProvider'; +import { ProviderType } from './ProviderType'; +import logger from '../logger'; +import { OpenAIAudioClient, AudioTranscriptionResponse } from '../clients/openai-audio-client'; + +export class OpenAIAudioProvider extends BaseProvider { + private audioClient: OpenAIAudioClient; + + constructor(stream: boolean, model: string) { + super(stream, model); + const apiKey = this.getApiKey(); + if (!apiKey) { + throw new Error('OpenAI API key is required for audio provider'); + } + this.audioClient = new OpenAIAudioClient(apiKey); + } + + getType(): ProviderType { + return ProviderType.OPENAI_AUDIO; + } + + getBaseUrl(): string { + return this.OPENAI_BASE_URL; + } + + getApiKey(): string | undefined { + return process.env.OPENAI_API_KEY; + } + + async handleBody(data: string): Promise { + try { + const audioResponse: AudioTranscriptionResponse = JSON.parse(data); + + + const duration = audioResponse.duration || 0; + const durationMinutes = duration / 60; + + // Cost per minute for Whisper models (as per OpenAI pricing) + const costPerMinute = 0.006; + const totalCost = new Decimal(durationMinutes * costPerMinute); + + const transaction: Transaction = { + metadata: { + providerId: 'openai-audio', + provider: 'openai', + model: this.getModel(), + durationSeconds: durationMinutes * 60, + generateAudio: false + }, + rawTransactionCost: totalCost, + status: 'success', + }; + + logger.info('Audio transcription transaction:', { + model: this.getModel(), + cost: totalCost.toNumber(), + durationMinutes, + userId: this.getUserId(), + }); + + return transaction; + } catch (error) { + logger.error('Error processing audio response:', error); + throw error; + } + } + + override supportsStream(): boolean { + return false; // Audio transcription doesn't support streaming + } + + override ensureStreamUsage( + reqBody: Record, + reqPath: string + ): Record { + // Audio endpoints don't support streaming + return reqBody; + } + + override transformRequestBody( + reqBody: Record, + reqPath: string + ): Record { + // No transformation needed for audio requests + return reqBody; + } +} \ No newline at end of file diff --git a/packages/app/server/src/providers/ProviderFactory.ts b/packages/app/server/src/providers/ProviderFactory.ts index 7efcf3e6c..79dae75c4 100644 --- a/packages/app/server/src/providers/ProviderFactory.ts +++ b/packages/app/server/src/providers/ProviderFactory.ts @@ -3,6 +3,7 @@ import { ALL_SUPPORTED_IMAGE_MODELS, ALL_SUPPORTED_MODELS, ALL_SUPPORTED_VIDEO_MODELS, + ALL_SUPPORTED_AUDIO_MODELS, } from '../services/AccountingService'; import type { EchoControlService } from '../services/EchoControlService'; import { AnthropicGPTProvider } from './AnthropicGPTProvider'; @@ -17,6 +18,7 @@ import { PROXY_PASSTHROUGH_ONLY_MODEL as GeminiVeoProxyPassthroughOnlyModel, } from './GeminiVeoProvider'; import { GPTProvider } from './GPTProvider'; +import { OpenAIAudioProvider } from './OpenAIAudioProvider'; import { OpenAIImageProvider } from './OpenAIImageProvider'; import { OpenAIResponsesProvider } from './OpenAIResponsesProvider'; import { OpenRouterProvider } from './OpenRouterProvider'; @@ -91,6 +93,19 @@ const createVideoModelToProviderMapping = (): Record => { return mapping; }; + +const createAudioModelToProviderMapping = (): Record => { + const mapping: Record = {}; + + for (const modelConfig of ALL_SUPPORTED_AUDIO_MODELS) { + if (modelConfig.provider === 'OpenAI') { + mapping[modelConfig.model_id] = ProviderType.OPENAI_AUDIO; + } + } + + return mapping; +}; + /** * Model-to-provider mapping loaded from model_prices_and_context_window.json * This replaces the previous hardcoded mapping and automatically includes all @@ -105,6 +120,9 @@ export const IMAGE_MODEL_TO_PROVIDER: Record = export const VIDEO_MODEL_TO_PROVIDER: Record = createVideoModelToProviderMapping(); +export const AUDIO_MODEL_TO_PROVIDER: Record = + createAudioModelToProviderMapping(); + export const getProvider = ( model: string, stream: boolean, @@ -123,6 +141,11 @@ export const getProvider = ( type = videoType; } + const audioType = AUDIO_MODEL_TO_PROVIDER[model]; + if (audioType) { + type = audioType; + } + if (model === GeminiVeoProxyPassthroughOnlyModel) { type = ProviderType.GEMINI_VEO; } @@ -145,6 +168,11 @@ export const getProvider = ( type = ProviderType.OPENAI_IMAGES; } + // Check if this is an audio transcription or translation endpoint + if (completionPath.includes('audio/transcriptions') || completionPath.includes('audio/translations')) { + type = ProviderType.OPENAI_AUDIO; + } + // We select for Anthropic Native if the completionPath includes "messages" // The OpenAI Format does not hit /v1/messages, it hits /v1/chat/completions // but the anthropic native format hits /v1/messages @@ -184,6 +212,8 @@ export const getProvider = ( return new OpenAIVideoProvider(stream, model); case ProviderType.GROQ: return new GroqProvider(stream, model); + case ProviderType.OPENAI_AUDIO: + return new OpenAIAudioProvider(stream, model); default: throw new Error(`Unknown provider type: ${type}`); } diff --git a/packages/app/server/src/providers/ProviderType.ts b/packages/app/server/src/providers/ProviderType.ts index e8b006ab4..552234f1b 100644 --- a/packages/app/server/src/providers/ProviderType.ts +++ b/packages/app/server/src/providers/ProviderType.ts @@ -11,4 +11,5 @@ export enum ProviderType { OPENAI_IMAGES = 'OPENAI_IMAGES', OPENAI_VIDEOS = 'OPENAI_VIDEOS', GROQ = 'GROQ', + OPENAI_AUDIO = 'OPENAI_AUDIO', } diff --git a/packages/app/server/src/services/AccountingService.ts b/packages/app/server/src/services/AccountingService.ts index 8a3006475..3900eece2 100644 --- a/packages/app/server/src/services/AccountingService.ts +++ b/packages/app/server/src/services/AccountingService.ts @@ -5,10 +5,12 @@ import { OpenRouterModels, GroqModels, OpenAIImageModels, + OpenAIAudioModels, SupportedOpenAIResponseToolPricing, SupportedModel, SupportedImageModel, SupportedVideoModel, + SupportedAudioModel, } from '@merit-systems/echo-typescript-sdk'; import { Decimal } from '@prisma/client/runtime/library'; @@ -40,6 +42,10 @@ export const ALL_SUPPORTED_VIDEO_MODELS: SupportedVideoModel[] = [ ...OpenAIVideoModels, ]; +export const ALL_SUPPORTED_AUDIO_MODELS: SupportedAudioModel[] = [ + ...OpenAIAudioModels, +]; + // Create a lookup map for O(1) model price retrieval const MODEL_PRICE_MAP = new Map(); ALL_SUPPORTED_MODELS.forEach(model => { diff --git a/packages/sdk/examples/next/src/app/components/audio.tsx b/packages/sdk/examples/next/src/app/components/audio.tsx new file mode 100644 index 000000000..cf727a5a4 --- /dev/null +++ b/packages/sdk/examples/next/src/app/components/audio.tsx @@ -0,0 +1,258 @@ +'use client'; + +import { useState, useRef } from 'react'; + +export default function AudioTranscription() { + const [transcription, setTranscription] = useState(''); + const [isRecording, setIsRecording] = useState(false); + const [loading, setLoading] = useState(false); + const [selectedFile, setSelectedFile] = useState(null); + const [model, setModel] = useState<'whisper-1' | 'whisper-large-v3'>('whisper-1'); + const [mode, setMode] = useState<'transcribe' | 'translate'>('transcribe'); + + const mediaRecorderRef = useRef(null); + const audioChunksRef = useRef([]); + const fileInputRef = useRef(null); + + // Start recording + const startRecording = async () => { + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + const mediaRecorder = new MediaRecorder(stream); + mediaRecorderRef.current = mediaRecorder; + audioChunksRef.current = []; + + mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + audioChunksRef.current.push(event.data); + } + }; + + mediaRecorder.onstop = () => { + const audioBlob = new Blob(audioChunksRef.current, { type: 'audio/wav' }); + const audioFile = new File([audioBlob], 'recording.wav', { type: 'audio/wav' }); + setSelectedFile(audioFile); + }; + + mediaRecorder.start(); + setIsRecording(true); + } catch (error) { + console.error('Error starting recording:', error); + alert('Error accessing microphone. Please ensure you have granted permission.'); + } + }; + + // Stop recording + const stopRecording = () => { + if (mediaRecorderRef.current && isRecording) { + mediaRecorderRef.current.stop(); + mediaRecorderRef.current.stream.getTracks().forEach(track => track.stop()); + setIsRecording(false); + } + }; + + // Handle file selection + const handleFileSelect = (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + if (file) { + // Check if it's an audio file + if (file.type.startsWith('audio/')) { + setSelectedFile(file); + } else { + alert('Please select an audio file'); + event.target.value = ''; + } + } + }; + + // Transcribe or translate audio + const processAudio = async () => { + if (!selectedFile) return; + + setLoading(true); + setTranscription(''); + + try { + const formData = new FormData(); + formData.append('file', selectedFile); + formData.append('model', model); + formData.append('response_format', 'text'); + + const endpoint = mode === 'transcribe' ? '/v1/audio/transcriptions' : '/v1/audio/translations'; + + const response = await fetch(endpoint, { + method: 'POST', + body: formData, + headers: { + 'Authorization': `Bearer ${process.env.NEXT_PUBLIC_ECHO_API_KEY}`, + }, + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + const result = await response.text(); + setTranscription(result); + } catch (error) { + console.error('Audio processing error:', error); + setTranscription('Error processing audio. Please try again.'); + } finally { + setLoading(false); + } + }; + + // Clear everything + const clearAll = () => { + setTranscription(''); + setSelectedFile(null); + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + }; + + return ( +
+ {/* Controls */} +
+ {/* Model Selection */} +
+ + +
+ + {/* Mode Selection */} +
+ + +
+
+ + {/* Audio Input Options */} +
+

Audio Input

+ + {/* Recording Controls */} +
+ + + {isRecording && ( +
+
+ Recording... +
+ )} +
+ + {/* File Upload */} +
+ + +
+ + {/* Selected File Info */} + {selectedFile && ( +
+

+ Selected file: {selectedFile.name} +

+

+ Size: {(selectedFile.size / 1024 / 1024).toFixed(2)} MB +

+
+ )} +
+ + {/* Process Button */} +
+ + + {(selectedFile || transcription) && ( + + )} +
+ + {/* Results */} + {transcription && ( +
+

+ {mode === 'transcribe' ? 'Transcription' : 'Translation'} Result +

+
+

{transcription}

+
+ +
+ )} + + {/* Info */} +
+

• Supported formats: MP3, WAV, M4A, OGG, and other common audio formats

+

• Maximum file size: 25 MB

+

• Cost: $0.006 per minute of audio

+
+
+ ); +} \ No newline at end of file diff --git a/packages/sdk/examples/next/src/app/components/tabs-container.tsx b/packages/sdk/examples/next/src/app/components/tabs-container.tsx index 61f9a855a..313e16195 100644 --- a/packages/sdk/examples/next/src/app/components/tabs-container.tsx +++ b/packages/sdk/examples/next/src/app/components/tabs-container.tsx @@ -4,9 +4,10 @@ import { useState } from 'react'; import Chat from './chat'; import ImageGenerator from './image'; +import AudioTranscription from './audio'; export default function TabsContainer() { - const [activeTab, setActiveTab] = useState<'chat' | 'image'>('chat'); + const [activeTab, setActiveTab] = useState<'chat' | 'image' | 'audio'>('chat'); return (
@@ -32,6 +33,16 @@ export default function TabsContainer() { > Image Generation +
{/* Tab Content (kept mounted to preserve state) */} @@ -48,6 +59,12 @@ export default function TabsContainer() { +
+

+ AI Audio Transcription +

+ +
); diff --git a/packages/sdk/examples/vite/src/App.tsx b/packages/sdk/examples/vite/src/App.tsx index 32a250020..5e6a78107 100644 --- a/packages/sdk/examples/vite/src/App.tsx +++ b/packages/sdk/examples/vite/src/App.tsx @@ -7,9 +7,10 @@ import { import { useState } from 'react'; import { ChatInterface } from './components/ChatInterface'; import { ImageGeneration } from './components/ImageGeneration'; +import { AudioTranscription } from './components/AudioTranscription'; import UseChatInterface from './components/UseChatInterface'; -type Tab = 'chat' | 'images' | 'use-chat'; +type Tab = 'chat' | 'images' | 'audio' | 'use-chat'; function Dashboard() { const { user, balance, error, isLoading } = useEcho(); @@ -128,6 +129,16 @@ function Dashboard() { > 🎨 Image Generation + + + {isRecording && ( +
+
+ Recording... +
+ )} + + + {/* File Upload */} +
+ + +
+ + {/* Selected File Info */} + {selectedFile && ( +
+

+ Selected file: {selectedFile.name} +

+

+ Size: {(selectedFile.size / 1024 / 1024).toFixed(2)} MB +

+
+ )} + + + {/* Process Button */} +
+ + + {(selectedFile || transcription) && ( + + )} +
+ + {/* Results */} + {transcription && ( +
+

+ {mode === 'transcribe' ? 'Transcription' : 'Translation'} Result +

+
+

{transcription}

+
+ +
+ )} + + {/* Info */} +
+

• Supported formats: MP3, WAV, M4A, OGG, and other common audio formats

+

• Maximum file size: 25 MB

+

• Cost: $0.006 per minute of audio

+
+ + ); +} \ No newline at end of file diff --git a/packages/sdk/ts/src/index.ts b/packages/sdk/ts/src/index.ts index 9dd11ce4f..0cdf26898 100644 --- a/packages/sdk/ts/src/index.ts +++ b/packages/sdk/ts/src/index.ts @@ -17,6 +17,7 @@ export type { SupportedToolType, SupportedModel, SupportedImageModel, + SupportedAudioModel, ImageGenerationQuality, ImageDimensions, WebSearchModel, @@ -54,3 +55,5 @@ export { VertexAIVideoModels } from './supported-models/video/vertex-ai'; export type { VertexAIVideoModel } from './supported-models/video/vertex-ai'; export { OpenAIVideoModels } from './supported-models/video/open-ai'; export type { OpenAIVideoModel } from './supported-models/video/open-ai'; +export { OpenAIAudioModels } from './supported-models/audio/openai'; +export type { OpenAIAudioModel } from './supported-models/audio/openai'; diff --git a/packages/sdk/ts/src/resources/models.ts b/packages/sdk/ts/src/resources/models.ts index ee4064d98..f0c96cef0 100644 --- a/packages/sdk/ts/src/resources/models.ts +++ b/packages/sdk/ts/src/resources/models.ts @@ -6,9 +6,11 @@ import { GeminiModels, OpenRouterModels, OpenAIImageModels, + OpenAIAudioModels, SupportedModel, SupportedImageModel, SupportedVideoModel, + SupportedAudioModel, GeminiVideoModels, } from '../supported-models'; @@ -38,4 +40,8 @@ export class ModelsResource extends BaseResource { async listSupportedVideoModels(): Promise { return GeminiVideoModels; } + + async listSupportedAudioModels(): Promise { + return OpenAIAudioModels; + } } diff --git a/packages/sdk/ts/src/supported-models/audio/openai.ts b/packages/sdk/ts/src/supported-models/audio/openai.ts new file mode 100644 index 000000000..afadf3450 --- /dev/null +++ b/packages/sdk/ts/src/supported-models/audio/openai.ts @@ -0,0 +1,16 @@ +import { SupportedAudioModel } from '../types'; + +export type OpenAIAudioModel = 'whisper-1' | 'whisper-large-v3'; + +export const OpenAIAudioModels: SupportedAudioModel[] = [ + { + model_id: 'whisper-1', + cost_per_minute: 0.006, + provider: 'OpenAI', + }, + { + model_id: 'whisper-large-v3', + cost_per_minute: 0.006, + provider: 'OpenAI', + }, +]; \ No newline at end of file diff --git a/packages/sdk/ts/src/supported-models/index.ts b/packages/sdk/ts/src/supported-models/index.ts index 3f641501d..cb9dc63eb 100644 --- a/packages/sdk/ts/src/supported-models/index.ts +++ b/packages/sdk/ts/src/supported-models/index.ts @@ -8,3 +8,4 @@ export * from './image/openai'; export * from './responses/openai'; export * from './video/gemini'; export * from './video/vertex-ai'; +export * from './audio/openai'; diff --git a/packages/sdk/ts/src/supported-models/types.ts b/packages/sdk/ts/src/supported-models/types.ts index 715f37664..2c836dbbb 100644 --- a/packages/sdk/ts/src/supported-models/types.ts +++ b/packages/sdk/ts/src/supported-models/types.ts @@ -84,3 +84,9 @@ export interface SupportedVideoModel { cost_per_second_without_audio: number; provider: string; } + +export interface SupportedAudioModel { + model_id: string; + cost_per_minute: number; + provider: string; +} diff --git a/packages/tests/provider-smoke/.env.example b/packages/tests/provider-smoke/.env.example new file mode 100644 index 000000000..aba64495d --- /dev/null +++ b/packages/tests/provider-smoke/.env.example @@ -0,0 +1,3 @@ +ECHO_API_KEY=your_echo_api_key_here +ECHO_APP_ID=your_echo_app_id_here +ECHO_DATA_SERVER_URL=https://echo.router.merit.systems diff --git a/packages/tests/provider-smoke/openai-audio-transcription.test.ts b/packages/tests/provider-smoke/openai-audio-transcription.test.ts new file mode 100644 index 000000000..eb0a4d131 --- /dev/null +++ b/packages/tests/provider-smoke/openai-audio-transcription.test.ts @@ -0,0 +1,175 @@ +import { describe, expect, it, beforeAll } from 'vitest'; +import fs from 'fs'; +import path from 'path'; +import { + ECHO_APP_ID, + assertEnv, + baseRouterUrl, + getApiErrorDetails, + getToken, +} from './test-helpers'; + +beforeAll(assertEnv); + +describe.concurrent('OpenAI Audio Transcription', () => { + const testAudioPath = path.join(__dirname, 'test-audio', 'sample.wav'); + + // Ensure test audio file exists + beforeAll(() => { + if (!fs.existsSync(testAudioPath)) { + throw new Error(`Test audio file not found: ${testAudioPath}`); + } + }); + + it('whisper-1 transcription', async () => { + try { + const audioBuffer = fs.readFileSync(testAudioPath); + const formData = new FormData(); + + // Create Blob from buffer for multipart upload + const blob = new Blob([audioBuffer], { type: 'audio/wav' }); + formData.append('file', blob, 'sample.wav'); + formData.append('model', 'whisper-1'); + formData.append('response_format', 'json'); + + const token = await getToken(); + const response = await fetch(`${baseRouterUrl}/audio/transcriptions`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'X-App-Id': ECHO_APP_ID!, + }, + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API error ${response.status}: ${errorText}`); + } + + const result = await response.json() as { text: string; duration?: number }; + + expect(result).toBeDefined(); + expect(result.text).toBeDefined(); + expect(typeof result.text).toBe('string'); + expect(result.text.length).toBeGreaterThan(0); + } catch (err) { + const details = getApiErrorDetails(err); + throw new Error(`[transcription] whisper-1 failed: ${details}`); + } + }); + + it('whisper-large-v3 transcription', async () => { + try { + const audioBuffer = fs.readFileSync(testAudioPath); + const formData = new FormData(); + + // Create Blob from buffer for multipart upload + const blob = new Blob([audioBuffer], { type: 'audio/wav' }); + formData.append('file', blob, 'sample.wav'); + formData.append('model', 'whisper-large-v3'); + formData.append('response_format', 'json'); + + const token = await getToken(); + const response = await fetch(`${baseRouterUrl}/audio/transcriptions`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'X-App-Id': ECHO_APP_ID!, + }, + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API error ${response.status}: ${errorText}`); + } + + const result = await response.json() as { text: string; duration?: number }; + + expect(result).toBeDefined(); + expect(result.text).toBeDefined(); + expect(typeof result.text).toBe('string'); + expect(result.text.length).toBeGreaterThan(0); + } catch (err) { + const details = getApiErrorDetails(err); + throw new Error(`[transcription] whisper-large-v3 failed: ${details}`); + } + }); + + it('whisper-1 translation', async () => { + try { + const audioBuffer = fs.readFileSync(testAudioPath); + const formData = new FormData(); + + // Create Blob from buffer for multipart upload + const blob = new Blob([audioBuffer], { type: 'audio/wav' }); + formData.append('file', blob, 'sample.wav'); + formData.append('model', 'whisper-1'); + formData.append('response_format', 'json'); + + const token = await getToken(); + const response = await fetch(`${baseRouterUrl}/audio/translations`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'X-App-Id': ECHO_APP_ID!, + }, + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API error ${response.status}: ${errorText}`); + } + + const result = await response.json() as { text: string; duration?: number }; + + expect(result).toBeDefined(); + expect(result.text).toBeDefined(); + expect(typeof result.text).toBe('string'); + expect(result.text.length).toBeGreaterThan(0); + } catch (err) { + const details = getApiErrorDetails(err); + throw new Error(`[translation] whisper-1 failed: ${details}`); + } + }); + + it('whisper-large-v3 translation', async () => { + try { + const audioBuffer = fs.readFileSync(testAudioPath); + const formData = new FormData(); + + // Create Blob from buffer for multipart upload + const blob = new Blob([audioBuffer], { type: 'audio/wav' }); + formData.append('file', blob, 'sample.wav'); + formData.append('model', 'whisper-large-v3'); + formData.append('response_format', 'json'); + + const token = await getToken(); + const response = await fetch(`${baseRouterUrl}/audio/translations`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'X-App-Id': ECHO_APP_ID!, + }, + body: formData, + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`API error ${response.status}: ${errorText}`); + } + + const result = await response.json() as { text: string; duration?: number }; + + expect(result).toBeDefined(); + expect(result.text).toBeDefined(); + expect(typeof result.text).toBe('string'); + expect(result.text.length).toBeGreaterThan(0); + } catch (err) { + const details = getApiErrorDetails(err); + throw new Error(`[translation] whisper-large-v3 failed: ${details}`); + } + }); +}); \ No newline at end of file diff --git a/packages/tests/provider-smoke/test-audio/sample.wav b/packages/tests/provider-smoke/test-audio/sample.wav new file mode 100644 index 0000000000000000000000000000000000000000..7f4aaabdf1bf13ac25cdb89ae392a5618422ff54 GIT binary patch literal 45260 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 yb;$n{l{X?nVZagcRPT)-2}xaKWGwgp=18'} - peerDependencies: - zod: ^3.25.76 || ^4 - '@ai-sdk/openai@2.0.32': resolution: {integrity: sha512-p7giSkCs66Q1qYO/NPYI41CrSg65mcm8R2uAdF86+Y1D1/q4mUrWMyf5UTOJ0bx/z4jIPiNgGDCg2Kabi5zrKQ==} engines: {node: '>=18'} @@ -1443,12 +1431,6 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 - '@ai-sdk/provider-utils@3.0.8': - resolution: {integrity: sha512-cDj1iigu7MW2tgAQeBzOiLhjHOUM9vENsgh4oAVitek0d//WdgfPCsKO3euP7m7LyO/j9a1vr/So+BGNdpFXYw==} - engines: {node: '>=18'} - peerDependencies: - zod: ^3.25.76 || ^4 - '@ai-sdk/provider-utils@3.0.9': resolution: {integrity: sha512-Pm571x5efqaI4hf9yW4KsVlDBDme8++UepZRnq+kqVBWWjgvGhQlzU8glaFq0YJEB9kkxZHbRRyVeHoV2sRYaQ==} engines: {node: '>=18'} @@ -7688,8 +7670,8 @@ packages: resolution: {integrity: sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A==} engines: {node: '>= 0.12'} - form-data@4.0.3: - resolution: {integrity: sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA==} + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} engines: {node: '>= 6'} format@0.2.2: @@ -7994,9 +7976,6 @@ packages: resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} - groq-sdk@0.33.0: - resolution: {integrity: sha512-wb7NrBq7LZDDhDPSpuAd9LpZ0MNjmWKGLfybYfjY3r63mSpfiP8+GQZQcSDJcX+jIMzSm+SwzxModDyVZ2T66Q==} - gtoken@7.1.0: resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} engines: {node: '>=14.0.0'} @@ -12018,12 +11997,6 @@ snapshots: '@ai-sdk/provider-utils': 3.0.9(zod@4.1.11) zod: 4.1.11 - '@ai-sdk/groq@2.0.17(zod@4.1.11)': - dependencies: - '@ai-sdk/provider': 2.0.0 - '@ai-sdk/provider-utils': 3.0.8(zod@4.1.11) - zod: 4.1.11 - '@ai-sdk/openai@2.0.32(zod@4.1.11)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -12038,13 +12011,6 @@ snapshots: zod: 4.1.11 zod-to-json-schema: 3.24.6(zod@4.1.11) - '@ai-sdk/provider-utils@3.0.8(zod@4.1.11)': - dependencies: - '@ai-sdk/provider': 2.0.0 - '@standard-schema/spec': 1.0.0 - eventsource-parser: 3.0.5 - zod: 4.1.11 - '@ai-sdk/provider-utils@3.0.9(zod@4.1.11)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -18685,7 +18651,7 @@ snapshots: '@types/node-fetch@2.6.12': dependencies: '@types/node': 20.19.16 - form-data: 4.0.3 + form-data: 4.0.4 '@types/node@12.20.55': {} @@ -18771,7 +18737,7 @@ snapshots: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 '@types/node': 20.19.16 - form-data: 4.0.3 + form-data: 4.0.4 '@types/supertest@6.0.3': dependencies: @@ -19143,14 +19109,14 @@ snapshots: chai: 5.2.0 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': + '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': dependencies: '@vitest/spy': 3.2.3 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: msw: 2.11.2(@types/node@20.19.16)(typescript@5.9.2) - vite: 6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) + vite: 6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0) '@vitest/mocker@3.2.3(msw@2.11.2(@types/node@24.3.1)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0))': dependencies: @@ -20840,7 +20806,7 @@ snapshots: axios@1.10.0: dependencies: follow-redirects: 1.15.9 - form-data: 4.0.3 + form-data: 4.0.4 proxy-from-env: 1.1.0 transitivePeerDependencies: - debug @@ -22637,7 +22603,7 @@ snapshots: mime-types: 2.1.35 safe-buffer: 5.2.1 - form-data@4.0.3: + form-data@4.0.4: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -22994,18 +22960,6 @@ snapshots: graphql@16.11.0: {} - groq-sdk@0.33.0: - dependencies: - '@types/node': 18.19.112 - '@types/node-fetch': 2.6.12 - abort-controller: 3.0.0 - agentkeepalive: 4.6.0 - form-data-encoder: 1.7.2 - formdata-node: 4.4.1 - node-fetch: 2.7.0 - transitivePeerDependencies: - - encoding - gtoken@7.1.0: dependencies: gaxios: 6.7.1 @@ -26892,7 +26846,7 @@ snapshots: cookiejar: 2.1.4 debug: 4.4.1 fast-safe-stringify: 2.1.1 - form-data: 4.0.3 + form-data: 4.0.4 formidable: 2.1.5 methods: 1.1.2 mime: 2.6.0 @@ -27880,7 +27834,7 @@ snapshots: dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.3 - '@vitest/mocker': 3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@20.19.16)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0)) + '@vitest/mocker': 3.2.3(msw@2.11.2(@types/node@20.19.16)(typescript@5.9.2))(vite@6.3.5(@types/node@24.3.1)(jiti@2.5.1)(lightningcss@1.30.1)(terser@5.42.0)(tsx@4.20.5)(yaml@2.8.0)) '@vitest/pretty-format': 3.2.3 '@vitest/runner': 3.2.3 '@vitest/snapshot': 3.2.3 diff --git a/tsx-0/17607-5f18cd68f2f3e65afb6a522ab737b6feeca74632 b/tsx-0/17607-5f18cd68f2f3e65afb6a522ab737b6feeca74632 new file mode 100644 index 000000000..3fa448a0d --- /dev/null +++ b/tsx-0/17607-5f18cd68f2f3e65afb6a522ab737b6feeca74632 @@ -0,0 +1 @@ +{"code":"#!/usr/bin/env tsx\nvar __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,\"name\",{value,configurable:true});import*as fs from\"fs\";import*as path from\"path\";import{execSync}from\"child_process\";import{fileURLToPath}from\"url\";const __filename=fileURLToPath(import.meta.url);const __dirname=path.dirname(__filename);const CONTROL_APP_PATH=\"../../../app/control\";const RESOLVED_TYPES_FILE=\"src/generated/api-types-resolved.ts\";const SDK_OUTPUT_FILE=\"src/api-types.ts\";function parseArgs(){const args=process.argv.slice(2);return{verbose:args.includes(\"--verbose\")||args.includes(\"-v\"),skipGeneration:args.includes(\"--skip-generation\")}}__name(parseArgs,\"parseArgs\");function log(message,options){if(options.verbose){console.log(`[generate-api-types] ${message}`)}}__name(log,\"log\");function generateControlAppTypes(options){if(options.skipGeneration){log(\"Skipping API type generation (--skip-generation flag)\",options);return}log(\"Generating API types in control app...\",options);try{const controlAppPath=path.resolve(__dirname,CONTROL_APP_PATH);if(!fs.existsSync(controlAppPath)){throw new Error(`Control app not found at: ${controlAppPath}`)}execSync(\"pnpm prisma:generate\",{cwd:controlAppPath,stdio:options.verbose?\"inherit\":\"pipe\"});execSync(\"npm run generate-api-types\",{cwd:controlAppPath,stdio:options.verbose?\"inherit\":\"pipe\"});log(\"\\u2705 Successfully generated API types in control app\",options)}catch(error){console.error(\"\\u274C Failed to generate API types in control app:\",error);process.exit(1)}}__name(generateControlAppTypes,\"generateControlAppTypes\");function copyResolvedTypes(options){log(\"Copying resolved API types to SDK...\",options);try{const controlAppPath=path.resolve(__dirname,CONTROL_APP_PATH);const resolvedTypesPath=path.join(controlAppPath,RESOLVED_TYPES_FILE);const sdkOutputPath=path.resolve(__dirname,\"..\",SDK_OUTPUT_FILE);if(!fs.existsSync(resolvedTypesPath)){throw new Error(`Resolved types file not found at: ${resolvedTypesPath}`)}const resolvedTypesContent=fs.readFileSync(resolvedTypesPath,\"utf8\");const sdkContent=`// Auto-generated API response types for Echo TypeScript SDK\n// This file is generated by running: npm run generate-api-types\n// Source: ${path.relative(process.cwd(),resolvedTypesPath)}\n// Do not edit this file manually - it will be overwritten\n\n${resolvedTypesContent.replace(/^\\/\\/ Auto-generated.*?\\n/,\"\").replace(/^\\/\\/ This file.*?\\n/,\"\").replace(/^\\/\\/ These types.*?\\n/,\"\").replace(/^\\/\\/ Do not edit.*?\\n/,\"\").replace(/^\\n+/,\"\")}`;const sdkSrcDir=path.dirname(sdkOutputPath);if(!fs.existsSync(sdkSrcDir)){fs.mkdirSync(sdkSrcDir,{recursive:true})}fs.writeFileSync(sdkOutputPath,sdkContent,\"utf8\");log(`\\u2705 Successfully copied API types to: ${path.relative(process.cwd(),sdkOutputPath)}`,options);const typeCount=(sdkContent.match(/^export (interface|type)/gm)||[]).length;log(`\\u{1F4DD} Generated ${typeCount} API types for SDK`,options)}catch(error){console.error(\"\\u274C Failed to copy resolved types to SDK:\",error);process.exit(1)}}__name(copyResolvedTypes,\"copyResolvedTypes\");function validateGeneratedTypes(options){log(\"Validating generated types...\",options);try{const sdkOutputPath=path.resolve(__dirname,\"..\",SDK_OUTPUT_FILE);if(!fs.existsSync(sdkOutputPath)){throw new Error(`Generated types file not found at: ${sdkOutputPath}`)}const content=fs.readFileSync(sdkOutputPath,\"utf8\");if(!content.includes(\"export interface\")&&!content.includes(\"export type\")){throw new Error(\"Generated types file does not contain any exported types\")}if(content.includes(\"import(\")){console.warn(\"\\u26A0\\uFE0F Generated types may contain unresolved import statements\")}log(\"\\u2705 Generated types validation passed\",options)}catch(error){console.error(\"\\u274C Generated types validation failed:\",error);process.exit(1)}}__name(validateGeneratedTypes,\"validateGeneratedTypes\");async function main(){const options=parseArgs();console.log(\"\\u{1F527} Generating API types for Echo TypeScript SDK...\");if(options.verbose){console.log(\"Options:\",options);console.log(\"Working directory:\",process.cwd());console.log(\"Script directory:\",__dirname)}generateControlAppTypes(options);copyResolvedTypes(options);validateGeneratedTypes(options);console.log(\"\\u{1F389} Successfully generated API types for SDK!\");console.log(`\\u{1F4C4} Types available at: ${SDK_OUTPUT_FILE}`)}__name(main,\"main\");if(import.meta.url===`file://${process.argv[1]}`){main().catch(error=>{console.error(\"\\u274C Script failed:\",error);process.exit(1)})}export{copyResolvedTypes,generateControlAppTypes,main,parseArgs,validateGeneratedTypes};\n","warnings":[],"map":{"version":3,"mappings":"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","names":[],"ignoreList":[],"sources":["/root/developments/opensource/echo/packages/sdk/ts/scripts/generate-api-types.ts"],"sourcesContent":[null]}} \ No newline at end of file diff --git a/tsx-0/17607-e05636076a4a4019d7b276202483f8b60f7e7406 b/tsx-0/17607-e05636076a4a4019d7b276202483f8b60f7e7406 new file mode 100644 index 000000000..e69de29bb From 1624afc2322b38db1d72999b75033c4a45b2e883 Mon Sep 17 00:00:00 2001 From: Trynax Date: Sat, 18 Oct 2025 18:13:13 +0100 Subject: [PATCH 2/5] Add audio model validation and rebuild SDK --- .gitignore | 1 + .../server/src/services/AccountingService.ts | 10 + .../services/ProviderInitializationService.ts | 4 +- pnpm-lock.yaml | 199 ++++++++---------- ...7-5f18cd68f2f3e65afb6a522ab737b6feeca74632 | 1 - ...7-e05636076a4a4019d7b276202483f8b60f7e7406 | 0 6 files changed, 96 insertions(+), 119 deletions(-) delete mode 100644 tsx-0/17607-5f18cd68f2f3e65afb6a522ab737b6feeca74632 delete mode 100644 tsx-0/17607-e05636076a4a4019d7b276202483f8b60f7e7406 diff --git a/.gitignore b/.gitignore index b53e49e68..b459cb07d 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,4 @@ storybook-static .claude .turbo +tsx-0/ diff --git a/packages/app/server/src/services/AccountingService.ts b/packages/app/server/src/services/AccountingService.ts index 3900eece2..2ce5845ac 100644 --- a/packages/app/server/src/services/AccountingService.ts +++ b/packages/app/server/src/services/AccountingService.ts @@ -64,6 +64,12 @@ ALL_SUPPORTED_VIDEO_MODELS.forEach(model => { VIDEO_MODEL_MAP.set(model.model_id, model); }); +// Create a separate map for audio models +const AUDIO_MODEL_MAP = new Map(); +ALL_SUPPORTED_AUDIO_MODELS.forEach(model => { + AUDIO_MODEL_MAP.set(model.model_id, model); +}); + export const getModelPrice = (model: string) => { const supportedModel = MODEL_PRICE_MAP.get(model); @@ -118,6 +124,10 @@ export const isValidVideoModel = (model: string) => { return VIDEO_MODEL_MAP.has(model); }; +export const isValidAudioModel = (model: string) => { + return AUDIO_MODEL_MAP.has(model); +}; + export const getCostPerToken = ( model: string, inputTokens: number, diff --git a/packages/app/server/src/services/ProviderInitializationService.ts b/packages/app/server/src/services/ProviderInitializationService.ts index 1a1448ebf..bb3bba6b7 100644 --- a/packages/app/server/src/services/ProviderInitializationService.ts +++ b/packages/app/server/src/services/ProviderInitializationService.ts @@ -11,6 +11,7 @@ import { isValidImageModel, isValidModel, isValidVideoModel, + isValidAudioModel, } from './AccountingService'; import { extractIsStream, extractModelName } from './RequestDataService'; @@ -67,7 +68,8 @@ export async function initializeProvider( !model || (!isValidModel(model) && !isValidImageModel(model) && - !isValidVideoModel(model)) + !isValidVideoModel(model) && + !isValidAudioModel(model)) ) { logger.warn(`Invalid model: ${model}`); // if auth or x402 header, return 422 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b2d00e4ae..54e4eb7c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -67,7 +67,7 @@ importers: version: 2.10.0(@prisma/client@6.16.0(prisma@6.16.0(magicast@0.3.5)(typescript@5.9.2))(typescript@5.9.2)) '@coinbase/x402': specifier: ^0.6.4 - version: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10) '@hookform/resolvers': specifier: ^5.2.1 version: 5.2.1(react-hook-form@7.62.0(react@19.1.1)) @@ -419,7 +419,7 @@ importers: version: 1.34.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) '@coinbase/x402': specifier: ^0.6.5 - version: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@e2b/code-interpreter': specifier: ^2.0.1 version: 2.0.1 @@ -509,7 +509,7 @@ importers: version: 2.7.0 openai: specifier: ^6.2.0 - version: 6.2.0(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.11) + version: 6.2.0(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.11) prisma: specifier: 6.16.0 version: 6.16.0(magicast@0.3.5)(typescript@5.9.2) @@ -533,7 +533,7 @@ importers: version: 3.17.0 x402-express: specifier: ^0.6.5 - version: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + version: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: specifier: ^4.1.11 version: 4.1.11 @@ -1206,6 +1206,9 @@ importers: '@ai-sdk/google': specifier: 2.0.14 version: 2.0.14(zod@4.1.11) + '@ai-sdk/groq': + specifier: 2.0.17 + version: 2.0.17(zod@4.1.11) '@ai-sdk/openai': specifier: 2.0.32 version: 2.0.32(zod@4.1.11) @@ -1234,6 +1237,9 @@ importers: eslint: specifier: ^9.29.0 version: 9.35.0(jiti@2.5.1) + groq-sdk: + specifier: ^0.33.0 + version: 0.33.0 tsup: specifier: ^8.5.0 version: 8.5.0(@microsoft/api-extractor@7.52.8(@types/node@24.3.1))(jiti@2.5.1)(postcss@8.5.6)(tsx@4.20.5)(typescript@5.9.2)(yaml@2.8.0) @@ -1419,6 +1425,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 + '@ai-sdk/groq@2.0.17': + resolution: {integrity: sha512-Oh6Fk988KNvqy4w1crBhBQU8JIkfqhxSiYCbBZFqZSeDsagZ8SHsS2ni9+7pq6e0DR/XGp6fDGDFsm+01kmsmg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@ai-sdk/openai@2.0.32': resolution: {integrity: sha512-p7giSkCs66Q1qYO/NPYI41CrSg65mcm8R2uAdF86+Y1D1/q4mUrWMyf5UTOJ0bx/z4jIPiNgGDCg2Kabi5zrKQ==} engines: {node: '>=18'} @@ -1431,6 +1443,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 + '@ai-sdk/provider-utils@3.0.8': + resolution: {integrity: sha512-cDj1iigu7MW2tgAQeBzOiLhjHOUM9vENsgh4oAVitek0d//WdgfPCsKO3euP7m7LyO/j9a1vr/So+BGNdpFXYw==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@ai-sdk/provider-utils@3.0.9': resolution: {integrity: sha512-Pm571x5efqaI4hf9yW4KsVlDBDme8++UepZRnq+kqVBWWjgvGhQlzU8glaFq0YJEB9kkxZHbRRyVeHoV2sRYaQ==} engines: {node: '>=18'} @@ -7976,6 +7994,9 @@ packages: resolution: {integrity: sha512-mS1lbMsxgQj6hge1XZ6p7GPhbrtFwUFYi3wRzXAC/FmYnyXMTvvI3td3rjmQ2u8ewXueaSvRPWaEcgVVOT9Jnw==} engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + groq-sdk@0.33.0: + resolution: {integrity: sha512-wb7NrBq7LZDDhDPSpuAd9LpZ0MNjmWKGLfybYfjY3r63mSpfiP8+GQZQcSDJcX+jIMzSm+SwzxModDyVZ2T66Q==} + gtoken@7.1.0: resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} engines: {node: '>=14.0.0'} @@ -11997,6 +12018,12 @@ snapshots: '@ai-sdk/provider-utils': 3.0.9(zod@4.1.11) zod: 4.1.11 + '@ai-sdk/groq@2.0.17(zod@4.1.11)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.8(zod@4.1.11) + zod: 4.1.11 + '@ai-sdk/openai@2.0.32(zod@4.1.11)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -12011,6 +12038,13 @@ snapshots: zod: 4.1.11 zod-to-json-schema: 3.24.6(zod@4.1.11) + '@ai-sdk/provider-utils@3.0.8(zod@4.1.11)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.5 + zod: 4.1.11 + '@ai-sdk/provider-utils@3.0.9(zod@4.1.11)': dependencies: '@ai-sdk/provider': 2.0.0 @@ -12626,11 +12660,11 @@ snapshots: - utf-8-validate - zod - '@coinbase/x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@coinbase/x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)': dependencies: '@coinbase/cdp-sdk': 1.34.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - x402: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -12666,11 +12700,11 @@ snapshots: - utf-8-validate - ws - '@coinbase/x402@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@coinbase/x402@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@coinbase/cdp-sdk': 1.34.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - x402: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -17392,10 +17426,6 @@ snapshots: '@socket.io/component-emitter@3.1.2': {} - '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) @@ -17404,13 +17434,9 @@ snapshots: dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - - '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/compute-budget@0.8.0(@solana/kit@2.3.0(typescript@5.9.2))': dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 2.3.0(typescript@5.9.2) '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2))': dependencies: @@ -17422,13 +17448,9 @@ snapshots: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/token-2022@0.4.2(@solana/kit@2.3.0(typescript@5.9.2))': dependencies: - '@solana/kit': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - - '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': - dependencies: - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 2.3.0(typescript@5.9.2) '@solana-program/token@0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': dependencies: @@ -17438,9 +17460,9 @@ snapshots: dependencies: '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana-program/token@0.5.1(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))': + '@solana-program/token@0.5.1(@solana/kit@2.3.0(typescript@5.9.2))': dependencies: - '@solana/kit': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 2.3.0(typescript@5.9.2) '@solana/accounts@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)': dependencies: @@ -17597,31 +17619,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/instructions': 2.3.0(typescript@5.9.2) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/programs': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - '@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -17672,7 +17669,7 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/kit@2.3.0(typescript@5.9.2)': dependencies: '@solana/accounts': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -17685,11 +17682,11 @@ snapshots: '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/rpc-parsed-types': 2.3.0(typescript@5.9.2) '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/signers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/sysvars': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-confirmation': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.3.0(typescript@5.9.2) '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) typescript: 5.9.2 @@ -17779,15 +17776,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) - '@solana/subscribable': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@solana/rpc-subscriptions-channel-websocket@2.3.0(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) @@ -17814,24 +17802,6 @@ snapshots: '@solana/subscribable': 2.3.0(typescript@5.9.2) typescript: 5.9.2 - '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/fast-stable-stringify': 2.3.0(typescript@5.9.2) - '@solana/functional': 2.3.0(typescript@5.9.2) - '@solana/promises': 2.3.0(typescript@5.9.2) - '@solana/rpc-spec-types': 2.3.0(typescript@5.9.2) - '@solana/rpc-subscriptions-api': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions-channel-websocket': 2.3.0(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-subscriptions-spec': 2.3.0(typescript@5.9.2) - '@solana/rpc-transformers': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/subscribable': 2.3.0(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - '@solana/rpc-subscriptions@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/errors': 2.3.0(typescript@5.9.2) @@ -17974,23 +17944,6 @@ snapshots: transitivePeerDependencies: - fastestsmallesttextencoderdecoder - '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))': - dependencies: - '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/errors': 2.3.0(typescript@5.9.2) - '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/promises': 2.3.0(typescript@5.9.2) - '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - typescript: 5.9.2 - transitivePeerDependencies: - - fastestsmallesttextencoderdecoder - - ws - '@solana/transaction-confirmation@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -18025,7 +17978,7 @@ snapshots: - fastestsmallesttextencoderdecoder - ws - '@solana/transaction-confirmation@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))': + '@solana/transaction-confirmation@2.3.0(typescript@5.9.2)': dependencies: '@solana/addresses': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/codecs-strings': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -18033,7 +17986,7 @@ snapshots: '@solana/keys': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/promises': 2.3.0(typescript@5.9.2) '@solana/rpc': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) - '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/rpc-subscriptions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)) '@solana/rpc-types': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transaction-messages': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) '@solana/transactions': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2) @@ -22960,6 +22913,18 @@ snapshots: graphql@16.11.0: {} + groq-sdk@0.33.0: + dependencies: + '@types/node': 18.19.112 + '@types/node-fetch': 2.6.12 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + gtoken@7.1.0: dependencies: gaxios: 6.7.1 @@ -25007,9 +24972,9 @@ snapshots: ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) zod: 4.1.11 - openai@6.2.0(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.11): + openai@6.2.0(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.1.11): optionalDependencies: - ws: 7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10) + ws: 8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) zod: 4.1.11 openapi-fetch@0.13.8: @@ -28289,13 +28254,13 @@ snapshots: bufferutil: 4.0.9 utf-8-validate: 5.0.10 - x402-express@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402-express@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@coinbase/cdp-sdk': 1.34.0(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10) - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) express: 4.21.2 viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) - x402: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + x402: 0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) zod: 3.25.76 transitivePeerDependencies: - '@azure/app-configuration' @@ -28538,14 +28503,14 @@ snapshots: - utf-8-validate - ws - x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402@0.6.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10): dependencies: '@scure/base': 1.2.6 - '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': 0.5.1(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana/kit': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-confirmation': 2.3.0(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(typescript@5.9.2)) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(typescript@5.9.2)) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(typescript@5.9.2)) + '@solana/kit': 2.3.0(typescript@5.9.2) + '@solana/transaction-confirmation': 2.3.0(typescript@5.9.2) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: 2.17.5(@tanstack/react-query@5.90.2(react@19.1.1))(@types/react@19.1.10)(@vercel/blob@0.25.1)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11))(zod@3.25.76) zod: 3.25.76 @@ -28582,14 +28547,14 @@ snapshots: - utf-8-validate - ws - x402@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)): + x402@0.6.5(bufferutil@4.0.9)(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(utf-8-validate@5.0.10)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)): dependencies: '@scure/base': 1.2.6 - '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10))) - '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) - '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@7.5.10(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana-program/compute-budget': 0.8.0(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token': 0.5.1(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10))) + '@solana-program/token-2022': 0.4.2(@solana/kit@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)))(@solana/sysvars@2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)) + '@solana/kit': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) + '@solana/transaction-confirmation': 2.3.0(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.9.2)(ws@8.18.2(bufferutil@4.0.9)(utf-8-validate@5.0.10)) viem: 2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.25.76) wagmi: 2.17.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.33.3(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@4.1.11))(zod@3.25.76) zod: 3.25.76 diff --git a/tsx-0/17607-5f18cd68f2f3e65afb6a522ab737b6feeca74632 b/tsx-0/17607-5f18cd68f2f3e65afb6a522ab737b6feeca74632 deleted file mode 100644 index 3fa448a0d..000000000 --- a/tsx-0/17607-5f18cd68f2f3e65afb6a522ab737b6feeca74632 +++ /dev/null @@ -1 +0,0 @@ -{"code":"#!/usr/bin/env tsx\nvar __defProp=Object.defineProperty;var __name=(target,value)=>__defProp(target,\"name\",{value,configurable:true});import*as fs from\"fs\";import*as path from\"path\";import{execSync}from\"child_process\";import{fileURLToPath}from\"url\";const __filename=fileURLToPath(import.meta.url);const __dirname=path.dirname(__filename);const CONTROL_APP_PATH=\"../../../app/control\";const RESOLVED_TYPES_FILE=\"src/generated/api-types-resolved.ts\";const SDK_OUTPUT_FILE=\"src/api-types.ts\";function parseArgs(){const args=process.argv.slice(2);return{verbose:args.includes(\"--verbose\")||args.includes(\"-v\"),skipGeneration:args.includes(\"--skip-generation\")}}__name(parseArgs,\"parseArgs\");function log(message,options){if(options.verbose){console.log(`[generate-api-types] ${message}`)}}__name(log,\"log\");function generateControlAppTypes(options){if(options.skipGeneration){log(\"Skipping API type generation (--skip-generation flag)\",options);return}log(\"Generating API types in control app...\",options);try{const controlAppPath=path.resolve(__dirname,CONTROL_APP_PATH);if(!fs.existsSync(controlAppPath)){throw new Error(`Control app not found at: ${controlAppPath}`)}execSync(\"pnpm prisma:generate\",{cwd:controlAppPath,stdio:options.verbose?\"inherit\":\"pipe\"});execSync(\"npm run generate-api-types\",{cwd:controlAppPath,stdio:options.verbose?\"inherit\":\"pipe\"});log(\"\\u2705 Successfully generated API types in control app\",options)}catch(error){console.error(\"\\u274C Failed to generate API types in control app:\",error);process.exit(1)}}__name(generateControlAppTypes,\"generateControlAppTypes\");function copyResolvedTypes(options){log(\"Copying resolved API types to SDK...\",options);try{const controlAppPath=path.resolve(__dirname,CONTROL_APP_PATH);const resolvedTypesPath=path.join(controlAppPath,RESOLVED_TYPES_FILE);const sdkOutputPath=path.resolve(__dirname,\"..\",SDK_OUTPUT_FILE);if(!fs.existsSync(resolvedTypesPath)){throw new Error(`Resolved types file not found at: ${resolvedTypesPath}`)}const resolvedTypesContent=fs.readFileSync(resolvedTypesPath,\"utf8\");const sdkContent=`// Auto-generated API response types for Echo TypeScript SDK\n// This file is generated by running: npm run generate-api-types\n// Source: ${path.relative(process.cwd(),resolvedTypesPath)}\n// Do not edit this file manually - it will be overwritten\n\n${resolvedTypesContent.replace(/^\\/\\/ Auto-generated.*?\\n/,\"\").replace(/^\\/\\/ This file.*?\\n/,\"\").replace(/^\\/\\/ These types.*?\\n/,\"\").replace(/^\\/\\/ Do not edit.*?\\n/,\"\").replace(/^\\n+/,\"\")}`;const sdkSrcDir=path.dirname(sdkOutputPath);if(!fs.existsSync(sdkSrcDir)){fs.mkdirSync(sdkSrcDir,{recursive:true})}fs.writeFileSync(sdkOutputPath,sdkContent,\"utf8\");log(`\\u2705 Successfully copied API types to: ${path.relative(process.cwd(),sdkOutputPath)}`,options);const typeCount=(sdkContent.match(/^export (interface|type)/gm)||[]).length;log(`\\u{1F4DD} Generated ${typeCount} API types for SDK`,options)}catch(error){console.error(\"\\u274C Failed to copy resolved types to SDK:\",error);process.exit(1)}}__name(copyResolvedTypes,\"copyResolvedTypes\");function validateGeneratedTypes(options){log(\"Validating generated types...\",options);try{const sdkOutputPath=path.resolve(__dirname,\"..\",SDK_OUTPUT_FILE);if(!fs.existsSync(sdkOutputPath)){throw new Error(`Generated types file not found at: ${sdkOutputPath}`)}const content=fs.readFileSync(sdkOutputPath,\"utf8\");if(!content.includes(\"export interface\")&&!content.includes(\"export type\")){throw new Error(\"Generated types file does not contain any exported types\")}if(content.includes(\"import(\")){console.warn(\"\\u26A0\\uFE0F Generated types may contain unresolved import statements\")}log(\"\\u2705 Generated types validation passed\",options)}catch(error){console.error(\"\\u274C Generated types validation failed:\",error);process.exit(1)}}__name(validateGeneratedTypes,\"validateGeneratedTypes\");async function main(){const options=parseArgs();console.log(\"\\u{1F527} Generating API types for Echo TypeScript SDK...\");if(options.verbose){console.log(\"Options:\",options);console.log(\"Working directory:\",process.cwd());console.log(\"Script directory:\",__dirname)}generateControlAppTypes(options);copyResolvedTypes(options);validateGeneratedTypes(options);console.log(\"\\u{1F389} Successfully generated API types for SDK!\");console.log(`\\u{1F4C4} Types available at: ${SDK_OUTPUT_FILE}`)}__name(main,\"main\");if(import.meta.url===`file://${process.argv[1]}`){main().catch(error=>{console.error(\"\\u274C Script failed:\",error);process.exit(1)})}export{copyResolvedTypes,generateControlAppTypes,main,parseArgs,validateGeneratedTypes};\n","warnings":[],"map":{"version":3,"mappings":"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","names":[],"ignoreList":[],"sources":["/root/developments/opensource/echo/packages/sdk/ts/scripts/generate-api-types.ts"],"sourcesContent":[null]}} \ No newline at end of file diff --git a/tsx-0/17607-e05636076a4a4019d7b276202483f8b60f7e7406 b/tsx-0/17607-e05636076a4a4019d7b276202483f8b60f7e7406 deleted file mode 100644 index e69de29bb..000000000 From d615c83d31df06b8cd15ac2d9783f15d5809f745 Mon Sep 17 00:00:00 2001 From: Trynax Date: Tue, 21 Oct 2025 11:07:18 +0100 Subject: [PATCH 3/5] addresses code review feedbac --- .../server/src/clients/openai-audio-client.ts | 128 ------------- .../src/providers/OpenAIAudioProvider.ts | 26 +-- .../next/src/app/components/audio.tsx | 8 +- .../openai-audio-transcription.test.ts | 180 +++--------------- 4 files changed, 46 insertions(+), 296 deletions(-) delete mode 100644 packages/app/server/src/clients/openai-audio-client.ts diff --git a/packages/app/server/src/clients/openai-audio-client.ts b/packages/app/server/src/clients/openai-audio-client.ts deleted file mode 100644 index 23e70524e..000000000 --- a/packages/app/server/src/clients/openai-audio-client.ts +++ /dev/null @@ -1,128 +0,0 @@ -import FormData from 'form-data'; -import { File } from 'buffer'; -import fetch from 'node-fetch'; - -export interface TranscriptionOptions { - model: 'whisper-1' | 'whisper-large-v3'; - language?: string; - prompt?: string; - response_format?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt'; - temperature?: number; -} - -export interface TranslationOptions { - model: 'whisper-1' | 'whisper-large-v3'; - prompt?: string; - response_format?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt'; - temperature?: number; -} - -export interface AudioTranscriptionResponse { - text: string; - task?: string; - language?: string; - duration?: number; - segments?: Array<{ - id: number; - seek: number; - start: number; - end: number; - text: string; - tokens: number[]; - temperature: number; - avg_logprob: number; - compression_ratio: number; - no_speech_prob: number; - }>; -} - -export class OpenAIAudioClient { - private apiKey: string; - private baseUrl: string; - - constructor(apiKey: string, baseUrl = 'https://api.openai.com/v1') { - this.apiKey = apiKey; - this.baseUrl = baseUrl; - } - - /** - * Transcribe audio using OpenAI Whisper API - */ - async transcribe( - audioBuffer: Buffer, - filename: string, - options: TranscriptionOptions - ): Promise { - const formData = new FormData(); - - // Create File from buffer for multipart upload - const file = new File([audioBuffer], filename, { type: 'audio/wav' }); - formData.append('file', file.stream(), { - filename: filename, - contentType: 'audio/wav', - }); - - formData.append('model', options.model); - - if (options.language) formData.append('language', options.language); - if (options.prompt) formData.append('prompt', options.prompt); - if (options.response_format) formData.append('response_format', options.response_format); - if (options.temperature !== undefined) formData.append('temperature', options.temperature.toString()); - - const response = await fetch(`${this.baseUrl}/audio/transcriptions`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${this.apiKey}`, - ...formData.getHeaders(), - }, - body: formData, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`OpenAI Audio API error: ${response.status} - ${errorText}`); - } - - return response.json() as Promise; - } - - /** - * Translate audio to English using OpenAI Whisper API - */ - async translate( - audioBuffer: Buffer, - filename: string, - options: TranslationOptions - ): Promise { - const formData = new FormData(); - - // Create File from buffer for multipart upload - const file = new File([audioBuffer], filename, { type: 'audio/wav' }); - formData.append('file', file.stream(), { - filename: filename, - contentType: 'audio/wav', - }); - - formData.append('model', options.model); - - if (options.prompt) formData.append('prompt', options.prompt); - if (options.response_format) formData.append('response_format', options.response_format); - if (options.temperature !== undefined) formData.append('temperature', options.temperature.toString()); - - const response = await fetch(`${this.baseUrl}/audio/translations`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${this.apiKey}`, - ...formData.getHeaders(), - }, - body: formData, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`OpenAI Audio API error: ${response.status} - ${errorText}`); - } - - return response.json() as Promise; - } -} \ No newline at end of file diff --git a/packages/app/server/src/providers/OpenAIAudioProvider.ts b/packages/app/server/src/providers/OpenAIAudioProvider.ts index 6ec4d6cf2..f078a9df8 100644 --- a/packages/app/server/src/providers/OpenAIAudioProvider.ts +++ b/packages/app/server/src/providers/OpenAIAudioProvider.ts @@ -3,10 +3,10 @@ import { Transaction } from '../types'; import { BaseProvider } from './BaseProvider'; import { ProviderType } from './ProviderType'; import logger from '../logger'; -import { OpenAIAudioClient, AudioTranscriptionResponse } from '../clients/openai-audio-client'; +import OpenAI from 'openai'; export class OpenAIAudioProvider extends BaseProvider { - private audioClient: OpenAIAudioClient; + private client: OpenAI; constructor(stream: boolean, model: string) { super(stream, model); @@ -14,7 +14,7 @@ export class OpenAIAudioProvider extends BaseProvider { if (!apiKey) { throw new Error('OpenAI API key is required for audio provider'); } - this.audioClient = new OpenAIAudioClient(apiKey); + this.client = new OpenAI({ apiKey }); } getType(): ProviderType { @@ -31,13 +31,14 @@ export class OpenAIAudioProvider extends BaseProvider { async handleBody(data: string): Promise { try { - const audioResponse: AudioTranscriptionResponse = JSON.parse(data); + const audioResponse = JSON.parse(data) as { + text: string; + duration?: number; + language?: string; + }; - - const duration = audioResponse.duration || 0; - const durationMinutes = duration / 60; - - // Cost per minute for Whisper models (as per OpenAI pricing) + const durationSeconds = audioResponse.duration || 0; + const durationMinutes = durationSeconds / 60; const costPerMinute = 0.006; const totalCost = new Decimal(durationMinutes * costPerMinute); @@ -46,7 +47,7 @@ export class OpenAIAudioProvider extends BaseProvider { providerId: 'openai-audio', provider: 'openai', model: this.getModel(), - durationSeconds: durationMinutes * 60, + durationSeconds, generateAudio: false }, rawTransactionCost: totalCost, @@ -68,14 +69,14 @@ export class OpenAIAudioProvider extends BaseProvider { } override supportsStream(): boolean { - return false; // Audio transcription doesn't support streaming + // OpenAI supports streaming for audio with verbose_json response format + return true; } override ensureStreamUsage( reqBody: Record, reqPath: string ): Record { - // Audio endpoints don't support streaming return reqBody; } @@ -83,7 +84,6 @@ export class OpenAIAudioProvider extends BaseProvider { reqBody: Record, reqPath: string ): Record { - // No transformation needed for audio requests return reqBody; } } \ No newline at end of file diff --git a/packages/sdk/examples/next/src/app/components/audio.tsx b/packages/sdk/examples/next/src/app/components/audio.tsx index cf727a5a4..abe6348a4 100644 --- a/packages/sdk/examples/next/src/app/components/audio.tsx +++ b/packages/sdk/examples/next/src/app/components/audio.tsx @@ -78,14 +78,14 @@ export default function AudioTranscription() { formData.append('model', model); formData.append('response_format', 'text'); - const endpoint = mode === 'transcribe' ? '/v1/audio/transcriptions' : '/v1/audio/translations'; + // Use the Next.js API proxy route + const endpoint = mode === 'transcribe' + ? '/api/echo/v1/audio/transcriptions' + : '/api/echo/v1/audio/translations'; const response = await fetch(endpoint, { method: 'POST', body: formData, - headers: { - 'Authorization': `Bearer ${process.env.NEXT_PUBLIC_ECHO_API_KEY}`, - }, }); if (!response.ok) { diff --git a/packages/tests/provider-smoke/openai-audio-transcription.test.ts b/packages/tests/provider-smoke/openai-audio-transcription.test.ts index eb0a4d131..7273bad69 100644 --- a/packages/tests/provider-smoke/openai-audio-transcription.test.ts +++ b/packages/tests/provider-smoke/openai-audio-transcription.test.ts @@ -1,6 +1,11 @@ import { describe, expect, it, beforeAll } from 'vitest'; import fs from 'fs'; import path from 'path'; +import { experimental_transcribe as transcribe } from 'ai'; +import { + OpenAIAudioModels, + createEchoOpenAI, +} from '@merit-systems/echo-typescript-sdk'; import { ECHO_APP_ID, assertEnv, @@ -14,162 +19,35 @@ beforeAll(assertEnv); describe.concurrent('OpenAI Audio Transcription', () => { const testAudioPath = path.join(__dirname, 'test-audio', 'sample.wav'); - // Ensure test audio file exists beforeAll(() => { if (!fs.existsSync(testAudioPath)) { throw new Error(`Test audio file not found: ${testAudioPath}`); } }); - it('whisper-1 transcription', async () => { - try { - const audioBuffer = fs.readFileSync(testAudioPath); - const formData = new FormData(); - - // Create Blob from buffer for multipart upload - const blob = new Blob([audioBuffer], { type: 'audio/wav' }); - formData.append('file', blob, 'sample.wav'); - formData.append('model', 'whisper-1'); - formData.append('response_format', 'json'); - - const token = await getToken(); - const response = await fetch(`${baseRouterUrl}/audio/transcriptions`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'X-App-Id': ECHO_APP_ID!, - }, - body: formData, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`API error ${response.status}: ${errorText}`); - } - - const result = await response.json() as { text: string; duration?: number }; - - expect(result).toBeDefined(); - expect(result.text).toBeDefined(); - expect(typeof result.text).toBe('string'); - expect(result.text.length).toBeGreaterThan(0); - } catch (err) { - const details = getApiErrorDetails(err); - throw new Error(`[transcription] whisper-1 failed: ${details}`); - } - }); - - it('whisper-large-v3 transcription', async () => { - try { - const audioBuffer = fs.readFileSync(testAudioPath); - const formData = new FormData(); - - // Create Blob from buffer for multipart upload - const blob = new Blob([audioBuffer], { type: 'audio/wav' }); - formData.append('file', blob, 'sample.wav'); - formData.append('model', 'whisper-large-v3'); - formData.append('response_format', 'json'); - - const token = await getToken(); - const response = await fetch(`${baseRouterUrl}/audio/transcriptions`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'X-App-Id': ECHO_APP_ID!, - }, - body: formData, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`API error ${response.status}: ${errorText}`); + const openai = createEchoOpenAI( + { appId: ECHO_APP_ID!, baseRouterUrl }, + getToken + ); + + // Test transcriptions for all audio models + for (const { model_id } of OpenAIAudioModels) { + it(`${model_id} transcription`, async () => { + try { + const audioFile = fs.readFileSync(testAudioPath); + + const { text } = await transcribe({ + model: openai.transcription(model_id), + audio: audioFile, + }); + + expect(text).toBeDefined(); + expect(typeof text).toBe('string'); + expect(text.length).toBeGreaterThan(0); + } catch (err) { + const details = getApiErrorDetails(err); + throw new Error(`[transcription] ${model_id} failed: ${details}`); } - - const result = await response.json() as { text: string; duration?: number }; - - expect(result).toBeDefined(); - expect(result.text).toBeDefined(); - expect(typeof result.text).toBe('string'); - expect(result.text.length).toBeGreaterThan(0); - } catch (err) { - const details = getApiErrorDetails(err); - throw new Error(`[transcription] whisper-large-v3 failed: ${details}`); - } - }); - - it('whisper-1 translation', async () => { - try { - const audioBuffer = fs.readFileSync(testAudioPath); - const formData = new FormData(); - - // Create Blob from buffer for multipart upload - const blob = new Blob([audioBuffer], { type: 'audio/wav' }); - formData.append('file', blob, 'sample.wav'); - formData.append('model', 'whisper-1'); - formData.append('response_format', 'json'); - - const token = await getToken(); - const response = await fetch(`${baseRouterUrl}/audio/translations`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'X-App-Id': ECHO_APP_ID!, - }, - body: formData, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`API error ${response.status}: ${errorText}`); - } - - const result = await response.json() as { text: string; duration?: number }; - - expect(result).toBeDefined(); - expect(result.text).toBeDefined(); - expect(typeof result.text).toBe('string'); - expect(result.text.length).toBeGreaterThan(0); - } catch (err) { - const details = getApiErrorDetails(err); - throw new Error(`[translation] whisper-1 failed: ${details}`); - } - }); - - it('whisper-large-v3 translation', async () => { - try { - const audioBuffer = fs.readFileSync(testAudioPath); - const formData = new FormData(); - - // Create Blob from buffer for multipart upload - const blob = new Blob([audioBuffer], { type: 'audio/wav' }); - formData.append('file', blob, 'sample.wav'); - formData.append('model', 'whisper-large-v3'); - formData.append('response_format', 'json'); - - const token = await getToken(); - const response = await fetch(`${baseRouterUrl}/audio/translations`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'X-App-Id': ECHO_APP_ID!, - }, - body: formData, - }); - - if (!response.ok) { - const errorText = await response.text(); - throw new Error(`API error ${response.status}: ${errorText}`); - } - - const result = await response.json() as { text: string; duration?: number }; - - expect(result).toBeDefined(); - expect(result.text).toBeDefined(); - expect(typeof result.text).toBe('string'); - expect(result.text.length).toBeGreaterThan(0); - } catch (err) { - const details = getApiErrorDetails(err); - throw new Error(`[translation] whisper-large-v3 failed: ${details}`); - } - }); + }); + } }); \ No newline at end of file From 98f919e710236d3dbfe43aa1c7b5dd52b8149e5f Mon Sep 17 00:00:00 2001 From: Trynax Date: Fri, 24 Oct 2025 01:40:13 +0100 Subject: [PATCH 4/5] audio pricing and Prisma path --- packages/app/server/src/services/EchoControlService.ts | 2 +- packages/app/server/src/services/PricingService.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/app/server/src/services/EchoControlService.ts b/packages/app/server/src/services/EchoControlService.ts index dca4e6558..8cbe67626 100644 --- a/packages/app/server/src/services/EchoControlService.ts +++ b/packages/app/server/src/services/EchoControlService.ts @@ -32,7 +32,7 @@ export class EchoControlService { constructor(db: PrismaClient, apiKey: string) { // Check if the generated Prisma client exists - const generatedPrismaPath = join(__dirname, 'generated', 'prisma'); + const generatedPrismaPath = join(__dirname, '..', 'generated', 'prisma'); if (!existsSync(generatedPrismaPath)) { throw new Error( `Generated Prisma client not found at ${generatedPrismaPath}. ` + diff --git a/packages/app/server/src/services/PricingService.ts b/packages/app/server/src/services/PricingService.ts index 98624ef79..910115bd5 100644 --- a/packages/app/server/src/services/PricingService.ts +++ b/packages/app/server/src/services/PricingService.ts @@ -5,6 +5,7 @@ import { getVideoModelPrice, isValidImageModel, isValidVideoModel, + isValidAudioModel, calculateToolCost, getImageModelPrice, } from './AccountingService'; @@ -20,8 +21,11 @@ export function getRequestMaxCost( provider: BaseProvider, isPassthroughProxyRoute: boolean ): Decimal { - // Need to switch between language/image/video for different pricing models. - if (isValidVideoModel(provider.getModel())) { + if (isValidAudioModel(provider.getModel())) { + const fileSizeBytes = Number(req.originalContentLength) || 1024 * 1024; + const estimatedMinutes = Math.max(1, fileSizeBytes / (1024 * 1024)); + return new Decimal(estimatedMinutes * 0.006); + } else if (isValidVideoModel(provider.getModel())) { const videoModelWithPricing = getVideoModelPrice(provider.getModel()); if (!videoModelWithPricing) { throw new UnknownModelError( From ec8168bf0961902dcd9a101bf964bfacbd56ccfd Mon Sep 17 00:00:00 2001 From: Trynax Date: Fri, 24 Oct 2025 03:16:51 +0100 Subject: [PATCH 5/5] force verbose_json format for audio duration field --- packages/app/server/src/providers/OpenAIAudioProvider.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/app/server/src/providers/OpenAIAudioProvider.ts b/packages/app/server/src/providers/OpenAIAudioProvider.ts index f078a9df8..471e5862e 100644 --- a/packages/app/server/src/providers/OpenAIAudioProvider.ts +++ b/packages/app/server/src/providers/OpenAIAudioProvider.ts @@ -84,6 +84,10 @@ export class OpenAIAudioProvider extends BaseProvider { reqBody: Record, reqPath: string ): Record { - return reqBody; + // Force verbose_json format to get duration field for cost calculation + return { + ...reqBody, + response_format: 'verbose_json' + }; } } \ No newline at end of file