From 61a97d73c63d5fe47a2349b4339905713892a3cd Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Mon, 20 Oct 2025 16:25:06 +0530 Subject: [PATCH 01/38] feat(server): add xAI Grok provider with X Search support --- .../server/src/providers/ProviderFactory.ts | 8 ++ .../app/server/src/providers/ProviderType.ts | 1 + .../app/server/src/providers/XAIProvider.ts | 105 ++++++++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 packages/app/server/src/providers/XAIProvider.ts diff --git a/packages/app/server/src/providers/ProviderFactory.ts b/packages/app/server/src/providers/ProviderFactory.ts index 7efcf3e6c..ffa6e96ca 100644 --- a/packages/app/server/src/providers/ProviderFactory.ts +++ b/packages/app/server/src/providers/ProviderFactory.ts @@ -21,6 +21,7 @@ import { OpenAIImageProvider } from './OpenAIImageProvider'; import { OpenAIResponsesProvider } from './OpenAIResponsesProvider'; import { OpenRouterProvider } from './OpenRouterProvider'; import { ProviderType } from './ProviderType'; +import { XAIProvider } from './XAIProvider'; import { VertexAIProvider, PROXY_PASSTHROUGH_ONLY_MODEL as VertexAIProxyPassthroughOnlyModel, @@ -52,6 +53,11 @@ const createChatModelToProviderMapping = (): Record => { case 'Groq': mapping[modelConfig.model_id] = ProviderType.GROQ; break; + case 'xAI': + case 'XAI': + case 'Xai': + mapping[modelConfig.model_id] = ProviderType.XAI; + break; // Add other providers as needed default: // Skip models with unsupported providers @@ -184,6 +190,8 @@ export const getProvider = ( return new OpenAIVideoProvider(stream, model); case ProviderType.GROQ: return new GroqProvider(stream, model); + case ProviderType.XAI: + return new XAIProvider(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..b2514ac80 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', + XAI = 'XAI', } diff --git a/packages/app/server/src/providers/XAIProvider.ts b/packages/app/server/src/providers/XAIProvider.ts new file mode 100644 index 000000000..97a589362 --- /dev/null +++ b/packages/app/server/src/providers/XAIProvider.ts @@ -0,0 +1,105 @@ +import { LlmTransactionMetadata, Transaction } from '../types'; +import { getCostPerToken } from '../services/AccountingService'; +import { BaseProvider } from './BaseProvider'; +import { ProviderType } from './ProviderType'; +import { CompletionStateBody, parseSSEGPTFormat } from './GPTProvider'; +import logger from '../logger'; + +export class XAIProvider extends BaseProvider { + private readonly XAI_BASE_URL = 'https://api.x.ai/v1'; + + getType(): ProviderType { + return ProviderType.XAI; + } + + getBaseUrl(): string { + return this.XAI_BASE_URL; + } + + getApiKey(): string | undefined { + return process.env.XAI_API_KEY; + } + + override supportsStream(): boolean { + return true; + } + + // Allow users to request X Search via OpenAI-like tool call named "web_search_preview" + override transformRequestBody( + reqBody: Record, + reqPath: string + ): Record { + try { + // If tools include web_search_preview, set xAI search flag on top-level + const tools = (reqBody as any)?.tools as unknown[] | undefined; + if (Array.isArray(tools)) { + const wantsWebSearch = tools.some(tool => { + const t = tool as any; + return ( + t?.type === 'web_search_preview' || t?.name === 'web_search_preview' + ); + }); + if (wantsWebSearch) { + (reqBody as any).search = true; + } + } + } catch (e) { + // best-effort; fall through + } + return reqBody; + } + + async handleBody(data: string): Promise { + try { + let prompt_tokens = 0; + let completion_tokens = 0; + let total_tokens = 0; + let providerId = 'null'; + + if (this.getIsStream()) { + const chunks = parseSSEGPTFormat(data); + + for (const chunk of chunks) { + if (chunk.usage !== null) { + prompt_tokens += chunk.usage.prompt_tokens; + completion_tokens += chunk.usage.completion_tokens; + total_tokens += chunk.usage.total_tokens; + } + providerId = chunk.id || 'null'; + } + } else { + const parsed = JSON.parse(data) as CompletionStateBody; + prompt_tokens += parsed.usage.prompt_tokens; + completion_tokens += parsed.usage.completion_tokens; + total_tokens += parsed.usage.total_tokens; + providerId = parsed.id || 'null'; + } + + const cost = getCostPerToken( + this.getModel(), + prompt_tokens, + completion_tokens + ); + + const metadata: LlmTransactionMetadata = { + providerId: providerId, + provider: this.getType(), + model: this.getModel(), + inputTokens: prompt_tokens, + outputTokens: completion_tokens, + totalTokens: total_tokens, + }; + + const transaction: Transaction = { + rawTransactionCost: cost, + metadata: metadata, + status: 'success', + }; + + return transaction; + } catch (error) { + logger.error(`Error processing data: ${error}`); + throw error; + } + } +} From 86bf1b656bf9dfc3b359f9dd5efcf3414bda6b95 Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Mon, 20 Oct 2025 16:27:32 +0530 Subject: [PATCH 02/38] feat(sdk): add xAI Grok model definitions and pricing --- packages/sdk/ts/src/index.ts | 3 + .../sdk/ts/src/supported-models/chat/xai.ts | 57 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 packages/sdk/ts/src/supported-models/chat/xai.ts diff --git a/packages/sdk/ts/src/index.ts b/packages/sdk/ts/src/index.ts index 9dd11ce4f..521a30f93 100644 --- a/packages/sdk/ts/src/index.ts +++ b/packages/sdk/ts/src/index.ts @@ -10,6 +10,7 @@ export * from './api-types'; export * from './utils/error-handling'; export * from './utils/validation'; export * from './providers'; +export { createEchoXAI } from './providers/xai'; // Export tool-related types and utilities export type { @@ -46,6 +47,8 @@ export { OpenRouterModels } from './supported-models/chat/openrouter'; export type { OpenRouterModel } from './supported-models/chat/openrouter'; export { GroqModels } from './supported-models/chat/groq'; export type { GroqModel } from './supported-models/chat/groq'; +export { XAIModels } from './supported-models/chat/xai'; +export type { XAIModel } from './supported-models/chat/xai'; export { OpenAIImageModels } from './supported-models/image/openai'; export type { OpenAIImageModel } from './supported-models/image/openai'; export { GeminiVideoModels } from './supported-models/video/gemini'; diff --git a/packages/sdk/ts/src/supported-models/chat/xai.ts b/packages/sdk/ts/src/supported-models/chat/xai.ts new file mode 100644 index 000000000..4b48b409f --- /dev/null +++ b/packages/sdk/ts/src/supported-models/chat/xai.ts @@ -0,0 +1,57 @@ +import { SupportedModel } from '../types'; + +// xAI Grok models and pricing (per 1 token). Source: https://docs.x.ai/docs/models +// Prices below are per-token, converted from $ per 1M tokens +export type XAIModel = + | 'grok-2-vision-1212' + | 'grok-3' + | 'grok-3-mini' + | 'grok-4-0709' + | 'grok-4-fast-non-reasoning' + | 'grok-4-fast-reasoning' + | 'grok-code-fast-1'; + +export const XAIModels: SupportedModel[] = [ + { + model_id: 'grok-2-vision-1212', + input_cost_per_token: 0.000002, // $2.00 / 1M + output_cost_per_token: 0.00001, // $10.00 / 1M + provider: 'xAI', + }, + { + model_id: 'grok-3', + input_cost_per_token: 0.000003, // $3.00 / 1M + output_cost_per_token: 0.000015, // $15.00 / 1M + provider: 'xAI', + }, + { + model_id: 'grok-3-mini', + input_cost_per_token: 0.0000003, // $0.30 / 1M + output_cost_per_token: 0.0000005, // $0.50 / 1M + provider: 'xAI', + }, + { + model_id: 'grok-4-0709', + input_cost_per_token: 0.000003, // $3.00 / 1M + output_cost_per_token: 0.000015, // $15.00 / 1M + provider: 'xAI', + }, + { + model_id: 'grok-4-fast-non-reasoning', + input_cost_per_token: 0.0000002, // $0.20 / 1M + output_cost_per_token: 0.0000005, // $0.50 / 1M + provider: 'xAI', + }, + { + model_id: 'grok-4-fast-reasoning', + input_cost_per_token: 0.0000002, // $0.20 / 1M + output_cost_per_token: 0.0000005, // $0.50 / 1M + provider: 'xAI', + }, + { + model_id: 'grok-code-fast-1', + input_cost_per_token: 0.0000002, // $0.20 / 1M + output_cost_per_token: 0.0000015, // $1.50 / 1M + provider: 'xAI', + }, +]; From e31f040f428d09e8072df82c734328b8512854cc Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Mon, 20 Oct 2025 16:30:58 +0530 Subject: [PATCH 03/38] feat(sdk): add xAI provider wrapper for TypeScript SDK --- .../server/src/services/AccountingService.ts | 2 ++ packages/sdk/ts/src/providers/index.ts | 3 ++ packages/sdk/ts/src/providers/xai.ts | 31 +++++++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 packages/sdk/ts/src/providers/xai.ts diff --git a/packages/app/server/src/services/AccountingService.ts b/packages/app/server/src/services/AccountingService.ts index 8a3006475..02e51e14e 100644 --- a/packages/app/server/src/services/AccountingService.ts +++ b/packages/app/server/src/services/AccountingService.ts @@ -9,6 +9,7 @@ import { SupportedModel, SupportedImageModel, SupportedVideoModel, + XAIModels, } from '@merit-systems/echo-typescript-sdk'; import { Decimal } from '@prisma/client/runtime/library'; @@ -28,6 +29,7 @@ export const ALL_SUPPORTED_MODELS: SupportedModel[] = [ ...GeminiModels, ...OpenRouterModels, ...GroqModels, + ...XAIModels, ]; // Handle image models separately since they have different pricing structure diff --git a/packages/sdk/ts/src/providers/index.ts b/packages/sdk/ts/src/providers/index.ts index 3c7d8a987..e52d6c7f6 100644 --- a/packages/sdk/ts/src/providers/index.ts +++ b/packages/sdk/ts/src/providers/index.ts @@ -1,6 +1,7 @@ export * from './anthropic'; export * from './google'; export * from './groq'; +export * from './xai'; export * from './openai'; export * from './openrouter'; @@ -61,3 +62,5 @@ export { type GoogleGenerativeAIProvider } from '@ai-sdk/google'; export { type GroqProvider } from '@ai-sdk/groq'; export { type OpenAIProvider } from '@ai-sdk/openai'; export { type OpenRouterProvider } from '@openrouter/ai-sdk-provider'; +// xAI uses a custom provider interface in our SDK (not from @ai-sdk) +export { type XAIProvider } from './xai'; diff --git a/packages/sdk/ts/src/providers/xai.ts b/packages/sdk/ts/src/providers/xai.ts new file mode 100644 index 000000000..501d141d5 --- /dev/null +++ b/packages/sdk/ts/src/providers/xai.ts @@ -0,0 +1,31 @@ +import { ROUTER_BASE_URL } from 'config'; +import { EchoConfig } from '../types'; +import { validateAppId } from '../utils/validation'; +import { echoFetch } from './index'; + +// xAI provider is OpenAI-compatible over our Echo router +export interface XAIProvider { + /** Base URL for the Echo router */ + baseURL: string; + /** Not used; replaced by echoFetch */ + apiKey: string; + fetch: typeof fetch; +} + +export function createEchoXAI( + { appId, baseRouterUrl = ROUTER_BASE_URL }: EchoConfig, + getTokenFn: (appId: string) => Promise, + onInsufficientFunds?: () => void +): XAIProvider { + validateAppId(appId, 'createEchoXAI'); + + return { + baseURL: baseRouterUrl, + apiKey: 'placeholder_replaced_by_echoFetch', + fetch: echoFetch( + fetch, + async () => await getTokenFn(appId), + onInsufficientFunds + ), + } as unknown as XAIProvider; +} From 8d11e86d74af742b18ee120b7b44abd95557b4b2 Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Mon, 20 Oct 2025 16:31:29 +0530 Subject: [PATCH 04/38] feat(sdk): add xAI provider to Next.js SDK --- packages/sdk/next/src/ai-providers/index.ts | 1 + packages/sdk/next/src/ai-providers/xai.ts | 10 ++++++++++ packages/sdk/next/src/index.ts | 2 ++ packages/sdk/next/src/types.ts | 2 ++ 4 files changed, 15 insertions(+) create mode 100644 packages/sdk/next/src/ai-providers/xai.ts diff --git a/packages/sdk/next/src/ai-providers/index.ts b/packages/sdk/next/src/ai-providers/index.ts index b3be66a25..4ea2555df 100644 --- a/packages/sdk/next/src/ai-providers/index.ts +++ b/packages/sdk/next/src/ai-providers/index.ts @@ -1,4 +1,5 @@ export * from './anthropic'; export * from './google'; +export * from './xai'; export * from './groq'; export * from './openai'; diff --git a/packages/sdk/next/src/ai-providers/xai.ts b/packages/sdk/next/src/ai-providers/xai.ts new file mode 100644 index 000000000..d79169c21 --- /dev/null +++ b/packages/sdk/next/src/ai-providers/xai.ts @@ -0,0 +1,10 @@ +import { getEchoToken } from '../auth/token-manager'; +import { + createEchoXAI as createEchoXAIBase, + EchoConfig, + XAIProvider, +} from '@merit-systems/echo-typescript-sdk'; + +export function createEchoXAI(config: EchoConfig): XAIProvider { + return createEchoXAIBase(config, async () => getEchoToken(config)); +} diff --git a/packages/sdk/next/src/index.ts b/packages/sdk/next/src/index.ts index 5b14aee5a..3417a75b8 100644 --- a/packages/sdk/next/src/index.ts +++ b/packages/sdk/next/src/index.ts @@ -8,6 +8,7 @@ import { createEchoGoogle } from 'ai-providers/google'; import { createEchoOpenAI } from 'ai-providers/openai'; import { createEchoOpenRouter } from 'ai-providers/openrouter'; import { createEchoGroq } from 'ai-providers/groq'; +import { createEchoXAI } from 'ai-providers/xai'; import { CreateOauthTokenResponse, @@ -116,5 +117,6 @@ export default function Echo(config: EchoConfig): EchoResult { google: createEchoGoogle(config), groq: createEchoGroq(config), openrouter: createEchoOpenRouter(config), + xai: createEchoXAI(config), }; } diff --git a/packages/sdk/next/src/types.ts b/packages/sdk/next/src/types.ts index e47dd6026..c5944ee83 100644 --- a/packages/sdk/next/src/types.ts +++ b/packages/sdk/next/src/types.ts @@ -5,6 +5,7 @@ import { CreateOauthTokenResponse, GroqProvider, OpenRouterProvider, + XAIProvider, } from '@merit-systems/echo-typescript-sdk'; import { NextRequest } from 'next/server'; @@ -50,4 +51,5 @@ export type EchoResult = { google: GoogleGenerativeAIProvider; groq: GroqProvider; openrouter: OpenRouterProvider; + xai: XAIProvider; }; From 1d670316fe6790c1e7c297f2c65c958abccbfd1c Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Mon, 20 Oct 2025 16:33:08 +0530 Subject: [PATCH 05/38] feat(sdk): integrate XAI tool support in React and TypeScript SDKs --- .../react/src/hooks/useEchoModelProviders.ts | 2 ++ .../sdk/ts/src/supported-models/tools/xai.ts | 32 +++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 packages/sdk/ts/src/supported-models/tools/xai.ts diff --git a/packages/sdk/react/src/hooks/useEchoModelProviders.ts b/packages/sdk/react/src/hooks/useEchoModelProviders.ts index 72d412aa2..c11631ce5 100644 --- a/packages/sdk/react/src/hooks/useEchoModelProviders.ts +++ b/packages/sdk/react/src/hooks/useEchoModelProviders.ts @@ -4,6 +4,7 @@ import { createEchoGroq, createEchoOpenAI, createEchoOpenRouter, + createEchoXAI, } from '@merit-systems/echo-typescript-sdk'; import { useMemo } from 'react'; import { useEcho } from './useEcho'; @@ -29,6 +30,7 @@ export const useEchoModelProviders = () => { onInsufficientFunds ), groq: createEchoGroq(baseConfig, getToken, onInsufficientFunds), + xai: createEchoXAI(baseConfig, getToken, onInsufficientFunds), }; }, [getToken, config.appId, config.baseRouterUrl, setIsInsufficientFunds]); }; diff --git a/packages/sdk/ts/src/supported-models/tools/xai.ts b/packages/sdk/ts/src/supported-models/tools/xai.ts new file mode 100644 index 000000000..e1b7817db --- /dev/null +++ b/packages/sdk/ts/src/supported-models/tools/xai.ts @@ -0,0 +1,32 @@ +import { SupportedTool, ToolPricing } from '../types'; + +export type XAITool = 'x_search'; + +export const XAITools: SupportedTool[] = [ + { + type: 'web_search_preview', + description: 'Search the X platform and web for real-time info', + pricing_structure: 'per_call', + }, +]; + +export const DefaultXAIToolPricing: ToolPricing = { + image_generation: { + gpt_image_1: { + low: { '1024x1024': 0, '1024x1536': 0, '1536x1024': 0 }, + medium: { '1024x1024': 0, '1024x1536': 0, '1536x1024': 0 }, + high: { '1024x1024': 0, '1024x1536': 0, '1536x1024': 0 }, + }, + }, + code_interpreter: { cost_per_session: 0 }, + file_search: { + cost_per_call: 0, + storage_cost_per_gb_per_day: 0, + free_storage_gb: 0, + }, + web_search_preview: { + gpt_4o: { cost_per_call: 0.025 }, + gpt_5: { cost_per_call: 0.01 }, + o_series: { cost_per_call: 0.01 }, + }, +}; From eac1e3ca4b523a0443046e2270347f3db78956b1 Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Mon, 20 Oct 2025 22:26:53 +0530 Subject: [PATCH 06/38] feat: add smoke tests for xAI streamText functionality --- .../provider-smoke/xai-stream-text.test.ts | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 packages/tests/provider-smoke/xai-stream-text.test.ts diff --git a/packages/tests/provider-smoke/xai-stream-text.test.ts b/packages/tests/provider-smoke/xai-stream-text.test.ts new file mode 100644 index 000000000..87f7854f5 --- /dev/null +++ b/packages/tests/provider-smoke/xai-stream-text.test.ts @@ -0,0 +1,64 @@ +import { createEchoXAI, XAIModels } from '@merit-systems/echo-typescript-sdk'; +import { streamText } from 'ai'; +import { beforeAll, describe, expect, it } from 'vitest'; +import { + ECHO_APP_ID, + assertEnv, + baseRouterUrl, + getApiErrorDetails, + getToken, +} from './test-helpers'; + +beforeAll(assertEnv); + +export const NON_CHAT_MODELS = [] as string[]; + +describe.concurrent('xAI (Grok) streamText per model', () => { + const xai = createEchoXAI({ appId: ECHO_APP_ID!, baseRouterUrl }, getToken); + + for (const { model_id } of XAIModels) { + if (NON_CHAT_MODELS.includes(model_id)) { + continue; + } + it(`xAI streamText ${model_id}`, async () => { + try { + const { textStream } = streamText({ + model: xai(model_id), + prompt: 'One-word greeting.', + }); + let streamed = ''; + for await (const d of textStream) streamed += d; + expect(streamed).toBeDefined(); + expect(streamed).not.toBe(''); + } catch (err) { + const details = getApiErrorDetails(err); + throw new Error(`[streamText] xAI ${model_id} failed: ${details}`); + } + }); + } + + // Basic coverage for X Search tool call translation + if (XAIModels.length > 0) { + const modelId = XAIModels[0]!.model_id; + it(`xAI streamText with web_search_preview tool: ${modelId}`, async () => { + try { + const { textStream } = streamText({ + model: xai(modelId), + prompt: 'Name a recent event on X (one sentence).', + // We use OpenAI-style web_search_preview; server translates to xAI search=true + tools: { + web_search_preview: {}, + } as any, + } as any); + let streamed = ''; + for await (const d of textStream) streamed += d; + expect(streamed).toBeDefined(); + } catch (err) { + const details = getApiErrorDetails(err); + throw new Error( + `[streamText+search] xAI ${modelId} failed: ${details}` + ); + } + }); + } +}); From b9799a1b59d91ea4293f03e57035450de839790e Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Mon, 20 Oct 2025 22:35:18 +0530 Subject: [PATCH 07/38] feat(sdk): update xAI provider integration and add dependency --- packages/sdk/ts/package.json | 1 + packages/sdk/ts/src/providers/index.ts | 3 +-- packages/sdk/ts/src/providers/xai.ts | 14 +++----------- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/packages/sdk/ts/package.json b/packages/sdk/ts/package.json index 93ea1657a..4a63b2690 100644 --- a/packages/sdk/ts/package.json +++ b/packages/sdk/ts/package.json @@ -61,6 +61,7 @@ "@ai-sdk/google": "2.0.14", "@ai-sdk/groq": "2.0.17", "@ai-sdk/openai": "2.0.32", + "@ai-sdk/xai": "2.0.16", "@openrouter/ai-sdk-provider": "1.2.0", "ai": "5.0.47" } diff --git a/packages/sdk/ts/src/providers/index.ts b/packages/sdk/ts/src/providers/index.ts index e52d6c7f6..645e92868 100644 --- a/packages/sdk/ts/src/providers/index.ts +++ b/packages/sdk/ts/src/providers/index.ts @@ -62,5 +62,4 @@ export { type GoogleGenerativeAIProvider } from '@ai-sdk/google'; export { type GroqProvider } from '@ai-sdk/groq'; export { type OpenAIProvider } from '@ai-sdk/openai'; export { type OpenRouterProvider } from '@openrouter/ai-sdk-provider'; -// xAI uses a custom provider interface in our SDK (not from @ai-sdk) -export { type XAIProvider } from './xai'; +export { type XAIProvider } from '@ai-sdk/xai'; diff --git a/packages/sdk/ts/src/providers/xai.ts b/packages/sdk/ts/src/providers/xai.ts index 501d141d5..6514223f6 100644 --- a/packages/sdk/ts/src/providers/xai.ts +++ b/packages/sdk/ts/src/providers/xai.ts @@ -1,17 +1,9 @@ +import { createXAI as createXAIBase, XAIProvider } from '@ai-sdk/xai'; import { ROUTER_BASE_URL } from 'config'; import { EchoConfig } from '../types'; import { validateAppId } from '../utils/validation'; import { echoFetch } from './index'; -// xAI provider is OpenAI-compatible over our Echo router -export interface XAIProvider { - /** Base URL for the Echo router */ - baseURL: string; - /** Not used; replaced by echoFetch */ - apiKey: string; - fetch: typeof fetch; -} - export function createEchoXAI( { appId, baseRouterUrl = ROUTER_BASE_URL }: EchoConfig, getTokenFn: (appId: string) => Promise, @@ -19,7 +11,7 @@ export function createEchoXAI( ): XAIProvider { validateAppId(appId, 'createEchoXAI'); - return { + return createXAIBase({ baseURL: baseRouterUrl, apiKey: 'placeholder_replaced_by_echoFetch', fetch: echoFetch( @@ -27,5 +19,5 @@ export function createEchoXAI( async () => await getTokenFn(appId), onInsufficientFunds ), - } as unknown as XAIProvider; + }); } From ed9057d9174504caaaa5678e5c72532357a651d1 Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Mon, 20 Oct 2025 22:57:09 +0530 Subject: [PATCH 08/38] remove deprecated web_search request handling. --- .../app/server/src/providers/XAIProvider.ts | 25 ------------------- 1 file changed, 25 deletions(-) diff --git a/packages/app/server/src/providers/XAIProvider.ts b/packages/app/server/src/providers/XAIProvider.ts index 97a589362..a5a4639d1 100644 --- a/packages/app/server/src/providers/XAIProvider.ts +++ b/packages/app/server/src/providers/XAIProvider.ts @@ -24,31 +24,6 @@ export class XAIProvider extends BaseProvider { return true; } - // Allow users to request X Search via OpenAI-like tool call named "web_search_preview" - override transformRequestBody( - reqBody: Record, - reqPath: string - ): Record { - try { - // If tools include web_search_preview, set xAI search flag on top-level - const tools = (reqBody as any)?.tools as unknown[] | undefined; - if (Array.isArray(tools)) { - const wantsWebSearch = tools.some(tool => { - const t = tool as any; - return ( - t?.type === 'web_search_preview' || t?.name === 'web_search_preview' - ); - }); - if (wantsWebSearch) { - (reqBody as any).search = true; - } - } - } catch (e) { - // best-effort; fall through - } - return reqBody; - } - async handleBody(data: string): Promise { try { let prompt_tokens = 0; From c285dd145451917d6e43534a8d602ea8f1262726 Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Mon, 20 Oct 2025 23:29:35 +0530 Subject: [PATCH 09/38] refactor: standardize naming for XaiProvider and update related imports --- packages/sdk/next/src/types.ts | 4 +-- packages/sdk/ts/src/providers/index.ts | 2 +- packages/sdk/ts/src/providers/xai.ts | 4 +-- .../provider-smoke/xai-stream-text.test.ts | 29 +++++++++++----- pnpm-lock.yaml | 34 +++++++++++++++++-- 5 files changed, 56 insertions(+), 17 deletions(-) diff --git a/packages/sdk/next/src/types.ts b/packages/sdk/next/src/types.ts index c5944ee83..5738bc5b8 100644 --- a/packages/sdk/next/src/types.ts +++ b/packages/sdk/next/src/types.ts @@ -5,7 +5,7 @@ import { CreateOauthTokenResponse, GroqProvider, OpenRouterProvider, - XAIProvider, + XaiProvider, } from '@merit-systems/echo-typescript-sdk'; import { NextRequest } from 'next/server'; @@ -51,5 +51,5 @@ export type EchoResult = { google: GoogleGenerativeAIProvider; groq: GroqProvider; openrouter: OpenRouterProvider; - xai: XAIProvider; + xai: XaiProvider; }; diff --git a/packages/sdk/ts/src/providers/index.ts b/packages/sdk/ts/src/providers/index.ts index 645e92868..62f54fac8 100644 --- a/packages/sdk/ts/src/providers/index.ts +++ b/packages/sdk/ts/src/providers/index.ts @@ -62,4 +62,4 @@ export { type GoogleGenerativeAIProvider } from '@ai-sdk/google'; export { type GroqProvider } from '@ai-sdk/groq'; export { type OpenAIProvider } from '@ai-sdk/openai'; export { type OpenRouterProvider } from '@openrouter/ai-sdk-provider'; -export { type XAIProvider } from '@ai-sdk/xai'; +export { type XaiProvider } from '@ai-sdk/xai'; diff --git a/packages/sdk/ts/src/providers/xai.ts b/packages/sdk/ts/src/providers/xai.ts index 6514223f6..6ff3163bb 100644 --- a/packages/sdk/ts/src/providers/xai.ts +++ b/packages/sdk/ts/src/providers/xai.ts @@ -1,4 +1,4 @@ -import { createXAI as createXAIBase, XAIProvider } from '@ai-sdk/xai'; +import { createXai as createXAIBase, XaiProvider } from '@ai-sdk/xai'; import { ROUTER_BASE_URL } from 'config'; import { EchoConfig } from '../types'; import { validateAppId } from '../utils/validation'; @@ -8,7 +8,7 @@ export function createEchoXAI( { appId, baseRouterUrl = ROUTER_BASE_URL }: EchoConfig, getTokenFn: (appId: string) => Promise, onInsufficientFunds?: () => void -): XAIProvider { +): XaiProvider { validateAppId(appId, 'createEchoXAI'); return createXAIBase({ diff --git a/packages/tests/provider-smoke/xai-stream-text.test.ts b/packages/tests/provider-smoke/xai-stream-text.test.ts index 87f7854f5..d97e6ff4b 100644 --- a/packages/tests/provider-smoke/xai-stream-text.test.ts +++ b/packages/tests/provider-smoke/xai-stream-text.test.ts @@ -37,22 +37,33 @@ describe.concurrent('xAI (Grok) streamText per model', () => { }); } - // Basic coverage for X Search tool call translation + // Test xAI Live Search with searchParameters (correct approach) if (XAIModels.length > 0) { const modelId = XAIModels[0]!.model_id; - it(`xAI streamText with web_search_preview tool: ${modelId}`, async () => { + it(`xAI streamText with Live Search (searchParameters): ${modelId}`, async () => { try { - const { textStream } = streamText({ + const { textStream, sources } = streamText({ model: xai(modelId), - prompt: 'Name a recent event on X (one sentence).', - // We use OpenAI-style web_search_preview; server translates to xAI search=true - tools: { - web_search_preview: {}, - } as any, - } as any); + prompt: 'What happened in tech news today? (one sentence)', + providerOptions: { + xai: { + searchParameters: { + mode: 'auto', + returnCitations: true, + sources: ['web', 'x'], + }, + }, + }, + }); let streamed = ''; for await (const d of textStream) streamed += d; expect(streamed).toBeDefined(); + expect(streamed).not.toBe(''); + + const resolvedSources = await sources; + if (resolvedSources && resolvedSources.length > 0) { + console.log(`✓ Search returned ${resolvedSources.length} sources`); + } } catch (err) { const details = getApiErrorDetails(err); throw new Error( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index bd9d05738..41a3ccdd4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1212,6 +1212,9 @@ importers: '@ai-sdk/openai': specifier: 2.0.32 version: 2.0.32(zod@4.1.11) + '@ai-sdk/xai': + specifier: 2.0.16 + version: 2.0.16(zod@4.1.11) '@openrouter/ai-sdk-provider': specifier: 1.2.0 version: 1.2.0(ai@5.0.47(zod@4.1.11))(zod@4.1.11) @@ -1431,6 +1434,12 @@ packages: peerDependencies: zod: ^3.25.76 || ^4 + '@ai-sdk/openai-compatible@1.0.15': + resolution: {integrity: sha512-i4TzohCxuFzBSdRNPa9eNFW6AYDZ5itbxz+rJa2kpNTMYqHgqKPGzet3X6eLIUVntA10icrqhWT+hUhxXZIS9Q==} + 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'} @@ -1479,6 +1488,12 @@ packages: zod: optional: true + '@ai-sdk/xai@2.0.16': + resolution: {integrity: sha512-t/Ohnn5OExgXZe+yhlpqOFZoixIXpaSBycWnvWfJ7JrpiNdg4WZEjWH+298zUXvqAT5wZvM93h1Ba4TkoYSyZg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -12024,6 +12039,12 @@ snapshots: '@ai-sdk/provider-utils': 3.0.8(zod@4.1.11) zod: 4.1.11 + '@ai-sdk/openai-compatible@1.0.15(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 @@ -12096,6 +12117,13 @@ snapshots: optionalDependencies: zod: 4.1.11 + '@ai-sdk/xai@2.0.16(zod@4.1.11)': + dependencies: + '@ai-sdk/openai-compatible': 1.0.15(zod@4.1.11) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.8(zod@4.1.11) + zod: 4.1.11 + '@alloc/quick-lru@5.2.0': {} '@ampproject/remapping@2.3.0': @@ -19143,14 +19171,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: @@ -27880,7 +27908,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 From 1d854aebd696a17cfddcb30b30fc5557730d885f Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Mon, 20 Oct 2025 23:33:18 +0530 Subject: [PATCH 10/38] temp fix --- .../sdk/ts/src/supported-models/tools/xai.ts | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 packages/sdk/ts/src/supported-models/tools/xai.ts diff --git a/packages/sdk/ts/src/supported-models/tools/xai.ts b/packages/sdk/ts/src/supported-models/tools/xai.ts deleted file mode 100644 index e1b7817db..000000000 --- a/packages/sdk/ts/src/supported-models/tools/xai.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { SupportedTool, ToolPricing } from '../types'; - -export type XAITool = 'x_search'; - -export const XAITools: SupportedTool[] = [ - { - type: 'web_search_preview', - description: 'Search the X platform and web for real-time info', - pricing_structure: 'per_call', - }, -]; - -export const DefaultXAIToolPricing: ToolPricing = { - image_generation: { - gpt_image_1: { - low: { '1024x1024': 0, '1024x1536': 0, '1536x1024': 0 }, - medium: { '1024x1024': 0, '1024x1536': 0, '1536x1024': 0 }, - high: { '1024x1024': 0, '1024x1536': 0, '1536x1024': 0 }, - }, - }, - code_interpreter: { cost_per_session: 0 }, - file_search: { - cost_per_call: 0, - storage_cost_per_gb_per_day: 0, - free_storage_gb: 0, - }, - web_search_preview: { - gpt_4o: { cost_per_call: 0.025 }, - gpt_5: { cost_per_call: 0.01 }, - o_series: { cost_per_call: 0.01 }, - }, -}; From c155f7d420a3bd3c3115462a4d948a4480aaeae2 Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Wed, 22 Oct 2025 22:05:29 +0530 Subject: [PATCH 11/38] Add Documentation for Echo Shadcn Components Registry --- .../control/docs/components/customization.mdx | 471 ++++++++++++++++++ .../control/docs/components/echo-account.mdx | 266 ++++++++++ .../app/control/docs/components/index.mdx | 60 +++ .../control/docs/components/installation.mdx | 165 ++++++ .../app/control/docs/components/meta.json | 11 + .../control/docs/components/ui-components.mdx | 325 ++++++++++++ packages/app/control/docs/meta.json | 3 +- 7 files changed, 1300 insertions(+), 1 deletion(-) create mode 100644 packages/app/control/docs/components/customization.mdx create mode 100644 packages/app/control/docs/components/echo-account.mdx create mode 100644 packages/app/control/docs/components/index.mdx create mode 100644 packages/app/control/docs/components/installation.mdx create mode 100644 packages/app/control/docs/components/meta.json create mode 100644 packages/app/control/docs/components/ui-components.mdx diff --git a/packages/app/control/docs/components/customization.mdx b/packages/app/control/docs/components/customization.mdx new file mode 100644 index 000000000..10f874cbd --- /dev/null +++ b/packages/app/control/docs/components/customization.mdx @@ -0,0 +1,471 @@ +--- +title: Customization +description: How to customize and theme Echo Components +--- + +# Customization + +Echo Components are designed to be highly customizable while maintaining consistency with the Echo design system. This guide covers various ways to customize components to match your application's needs. + +## Styling Approaches + +### 1. CSS Classes + +The most common way to customize Echo Components is through CSS classes: + +```tsx +import { Button } from '@/components/echo-button'; + +// Custom styling with Tailwind classes + +``` + +### 2. CSS Variables + +Override CSS variables for global theming: + +```css +:root { + --primary: 220 100% 50%; /* Custom primary color */ + --primary-foreground: 0 0% 100%; + --secondary: 220 14% 96%; + --secondary-foreground: 220 9% 46%; +} +``` + +### 3. Component Props + +Many components accept styling props: + +```tsx +import { MoneyInput } from '@/components/money-input'; + + +``` + +## Theme Customization + +### Light and Dark Themes + +Echo Components support both light and dark themes out of the box: + +```css +/* Light theme */ +:root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; +} + +/* Dark theme */ +.dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; +} +``` + +### Custom Color Schemes + +Create your own color schemes: + +```css +/* Brand colors */ +.brand-theme { + --primary: 142 76% 36%; /* Green primary */ + --primary-foreground: 355 7% 97%; + --secondary: 142 76% 36%; + --secondary-foreground: 355 7% 97%; +} + +/* Corporate theme */ +.corporate-theme { + --primary: 221 83% 53%; /* Blue primary */ + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96%; + --secondary-foreground: 222.2 47.4% 11.2%; +} +``` + +## Component-Specific Customization + +### Echo Button Customization + +```tsx +import { Button, buttonVariants } from '@/components/echo-button'; +import { cn } from '@/lib/utils'; + +// Custom variant +const customButtonVariants = cva( + buttonVariants.base, + { + variants: { + variant: { + ...buttonVariants.variants.variant, + custom: "bg-gradient-to-r from-purple-500 to-pink-500 text-white", + }, + }, + } +); + +// Usage + +``` + +### Money Input Customization + +```tsx +import { MoneyInput } from '@/components/money-input'; + +function CustomMoneyInput() { + return ( + + ); +} +``` + +### Echo Account Customization + +```tsx +import { EchoAccount } from '@/components/echo-account-react'; + +function CustomEchoAccount() { + return ( + + ); +} +``` + +## Advanced Customization + +### Creating Custom Components + +You can extend Echo Components to create your own: + +```tsx +import { Button } from '@/components/echo-button'; +import { cn } from '@/lib/utils'; + +interface CustomButtonProps extends React.ButtonHTMLAttributes { + loading?: boolean; + icon?: React.ReactNode; +} + +export function CustomButton({ + loading, + icon, + children, + className, + ...props +}: CustomButtonProps) { + return ( + + ); +} +``` + +### Custom Hooks + +Create custom hooks for component logic: + +```tsx +import { useState, useEffect } from 'react'; +import { useEcho } from '@merit-systems/echo-react-sdk'; + +export function useEchoAccount() { + const echo = useEcho(); + const [balance, setBalance] = useState(0); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (echo?.user) { + // Fetch balance + setLoading(false); + } + }, [echo?.user]); + + return { + balance, + loading, + user: echo?.user, + isAuthenticated: !!echo?.user, + }; +} +``` + +## Responsive Design + +### Mobile-First Approach + +Design components to work well on all screen sizes: + +```tsx +import { Button } from '@/components/echo-button'; + +function ResponsiveButton() { + return ( + + ); +} +``` + +### Breakpoint-Specific Styling + +```tsx +import { EchoAccount } from '@/components/echo-account-react'; + +function ResponsiveEchoAccount() { + return ( + + ); +} +``` + +## Performance Optimization + +### Memoization + +Use React.memo for components that don't need frequent re-renders: + +```tsx +import { memo } from 'react'; +import { Button } from '@/components/echo-button'; + +export const MemoizedButton = memo(Button); +``` + +### Lazy Loading + +Lazy load heavy components: + +```tsx +import { lazy, Suspense } from 'react'; + +const EchoAccount = lazy(() => import('@/components/echo-account-react')); + +function App() { + return ( + Loading...}> + + + ); +} +``` + +## Testing Customizations + +### Unit Tests + +Test your customizations with unit tests: + +```tsx +import { render, screen } from '@testing-library/react'; +import { CustomButton } from './CustomButton'; + +test('renders custom button with loading state', () => { + render(Click me); + + expect(screen.getByRole('button')).toBeDisabled(); + expect(screen.getByText('Click me')).toHaveClass('opacity-0'); +}); +``` + +### Visual Regression Tests + +Use tools like Chromatic or Percy to catch visual regressions: + +```tsx +import { CustomButton } from './CustomButton'; + +export const LoadingState = () => ( + Loading... +); + +export const WithIcon = () => ( + 🚀}>Launch +); +``` + +## Best Practices + +### Consistency + +- Maintain consistent spacing and typography +- Use the same color palette throughout your app +- Follow established patterns for similar components + +### Accessibility + +- Ensure custom colors meet contrast requirements +- Test with keyboard navigation +- Provide proper ARIA labels for custom components + +### Performance + +- Avoid unnecessary re-renders +- Use CSS classes instead of inline styles when possible +- Optimize images and assets + +### Maintenance + +- Document your customizations +- Use version control for component modifications +- Keep track of breaking changes in updates + +## Migration Guide + +### Updating Components + +When updating Echo Components: + +1. **Backup your customizations** - Save your custom code +2. **Check changelog** - Review what changed in the new version +3. **Test thoroughly** - Ensure your customizations still work +4. **Update gradually** - Update one component at a time + +### Breaking Changes + +Common breaking changes to watch for: + +- **Prop changes** - New required props or removed props +- **CSS class changes** - Updated class names +- **API changes** - Modified component interfaces + +### Rollback Strategy + +Have a rollback plan ready: + +```bash +# Keep previous versions +git tag v1.0.0 +git tag v1.1.0 + +# Rollback if needed +git checkout v1.0.0 +``` + +## Examples + +### Complete Custom Theme + +```css +/* custom-theme.css */ +:root { + /* Brand colors */ + --primary: 142 76% 36%; + --primary-foreground: 355 7% 97%; + --secondary: 142 76% 36%; + --secondary-foreground: 355 7% 97%; + + /* Custom spacing */ + --spacing-xs: 0.25rem; + --spacing-sm: 0.5rem; + --spacing-md: 1rem; + --spacing-lg: 1.5rem; + + /* Custom shadows */ + --shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05); + --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1); + --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1); +} +``` + +### Custom Component Library + +```tsx +// components/custom/Button.tsx +import { Button as EchoButton } from '@/components/echo-button'; +import { cn } from '@/lib/utils'; + +interface CustomButtonProps { + variant?: 'primary' | 'secondary' | 'danger'; + size?: 'sm' | 'md' | 'lg'; + loading?: boolean; +} + +export function CustomButton({ + variant = 'primary', + size = 'md', + loading = false, + className, + children, + ...props +}: CustomButtonProps) { + return ( + + {loading ? 'Loading...' : children} + + ); +} +``` \ No newline at end of file diff --git a/packages/app/control/docs/components/echo-account.mdx b/packages/app/control/docs/components/echo-account.mdx new file mode 100644 index 000000000..698c3870b --- /dev/null +++ b/packages/app/control/docs/components/echo-account.mdx @@ -0,0 +1,266 @@ +--- +title: Echo Account Component +description: Complete guide to the Echo Account component +--- + +# Echo Account Component + +The Echo Account component is a comprehensive user account management interface that provides everything you need for user authentication, balance display, and account management in your Echo applications. + +## Overview + +The Echo Account component includes: + +- **User authentication status** - Shows login/logout functionality +- **Balance display** - Real-time user balance with currency formatting +- **Top-up functionality** - Easy way for users to add funds +- **User profile management** - Avatar, name, and account details +- **Responsive design** - Works on desktop and mobile devices + +## Installation + +### React Version + +```bash +pnpm dlx shadcn@latest add https://echo-components.com/r/echo-account-react.json +``` + +### Next.js Version + +```bash +pnpm dlx shadcn@latest add https://echo-components.com/r/echo-account-next.json +``` + +## Basic Usage + +### React + +```tsx +import { EchoAccount } from '@/components/echo-account-react'; + +export default function MyApp() { + return ( +
+

My Echo App

+ +
+ ); +} +``` + +### Next.js + +```tsx +import { EchoAccount } from '@/components/echo-account-next'; + +export default function MyApp() { + return ( +
+

My Echo App

+ +
+ ); +} +``` + +## Component Structure + +The Echo Account component is composed of several sub-components: + +### EchoAccountButton + +The main button that triggers the account popover: + +```tsx +import { EchoAccountButton } from '@/components/echo-account-button/echo-account'; + +// Usage with Echo context +const echo = useEcho(); +return ; +``` + +### EchoPopover + +The popover container that holds the account interface: + +```tsx +import { EchoPopover } from '@/components/echo-account-button/echo-popover'; +``` + +### Balance Component + +Displays the user's current balance: + +```tsx +import { Balance } from '@/components/echo-account-button/balance'; +``` + +### TopUpButton + +Handles adding funds to the user's account: + +```tsx +import { TopUpButton } from '@/components/echo-account-button/top-up-button'; +``` + +## Customization + +### Styling + +You can customize the appearance using Tailwind classes: + +```tsx + +``` + +### Custom Props + +The component accepts various props for customization: + +```tsx +interface EchoAccountProps { + className?: string; + showBalance?: boolean; + showTopUp?: boolean; + variant?: 'default' | 'compact' | 'minimal'; +} +``` + +### Example with Custom Props + +```tsx + +``` + +## Advanced Usage + +### Custom Account Button + +You can create a custom account button with your own styling: + +```tsx +import { EchoAccountButton } from '@/components/echo-account-button/echo-account'; +import { useEcho } from '@merit-systems/echo-react-sdk'; + +function CustomAccountButton() { + const echo = useEcho(); + + return ( + + ); +} +``` + +### Standalone Components + +You can use individual components for custom layouts: + +```tsx +import { Balance } from '@/components/echo-account-button/balance'; +import { TopUpButton } from '@/components/echo-account-button/top-up-button'; + +function CustomAccountLayout() { + return ( +
+ + +
+ ); +} +``` + +## Integration with Echo SDK + +The Echo Account component automatically integrates with the Echo SDK: + +### Authentication + +- Automatically detects user login status +- Shows login button when user is not authenticated +- Displays user information when authenticated + +### Balance Management + +- Real-time balance updates +- Currency formatting +- Balance history (if available) + +### Payment Processing + +- Integrated top-up functionality +- Secure payment processing +- Transaction history + +## Responsive Design + +The Echo Account component is fully responsive: + +- **Desktop**: Full popover with all account details +- **Tablet**: Optimized layout for touch interfaces +- **Mobile**: Compact design with essential features + +## Accessibility + +The component includes comprehensive accessibility features: + +- **Keyboard navigation** - Full keyboard support +- **Screen reader support** - Proper ARIA labels and descriptions +- **Focus management** - Proper focus handling for popovers +- **Color contrast** - Meets WCAG guidelines + +## Best Practices + +### Performance + +- The component uses React.memo for optimal re-rendering +- Balance updates are debounced to prevent excessive API calls +- Lazy loading for non-critical components + +### Security + +- All user data is handled securely through the Echo SDK +- No sensitive information is stored in component state +- Proper error handling for failed operations + +### User Experience + +- Clear visual feedback for all user actions +- Loading states for async operations +- Error states with helpful messages + +## Troubleshooting + +### Common Issues + +**Component not rendering:** +- Ensure Echo provider is properly configured +- Check that user is authenticated +- Verify all required dependencies are installed + +**Balance not updating:** +- Check Echo SDK configuration +- Verify API endpoints are accessible +- Check browser console for errors + +**Styling issues:** +- Ensure Tailwind CSS is properly configured +- Check that component CSS is loaded +- Verify custom styles don't conflict + +### Debug Mode + +Enable debug mode to see detailed logging: + +```tsx + +``` + +This will log component state changes and API calls to the browser console. \ No newline at end of file diff --git a/packages/app/control/docs/components/index.mdx b/packages/app/control/docs/components/index.mdx new file mode 100644 index 000000000..a05b56214 --- /dev/null +++ b/packages/app/control/docs/components/index.mdx @@ -0,0 +1,60 @@ +--- +title: Echo Components +description: Pre-built UI components for Echo applications +--- + +# Echo Components + +Echo provides a comprehensive set of pre-built UI components through our [Shadcn Components Registry](https://echo-components.com/). These components are specifically designed for Echo applications and include everything you need to build beautiful, functional user interfaces. + +## What are Echo Components? + +Echo Components are pre-built React components that integrate seamlessly with the Echo ecosystem. They provide: + +- **Ready-to-use UI elements** - Drop-in components for common Echo app patterns +- **Echo integration** - Built-in support for Echo authentication, billing, and user management +- **Consistent design** - Unified design system across all Echo applications +- **Accessibility** - WCAG compliant components out of the box +- **Customization** - Easy theming and styling options + +## Available Components + +### Echo Account Components + +- **Echo Account Button** - Complete user account management with balance display, top-up functionality, and user profile +- **Echo Account (React)** - React-specific implementation with hooks integration +- **Echo Account (Next.js)** - Next.js optimized version with server-side rendering support + +### UI Components + +- **Echo Button** - Enhanced button component with Echo-specific variants +- **Money Input** - Specialized input for handling monetary values with proper formatting +- **Echo Logo** - Branded logo component with light/dark mode support + +## Quick Start + +To get started with Echo Components, you can install them using the Shadcn CLI: + +```bash +# Install Echo Account component for React +pnpm dlx shadcn@latest add https://echo-components.com/r/echo-account-react.json + +# Install Echo Account component for Next.js +pnpm dlx shadcn@latest add https://echo-components.com/r/echo-account-next.json +``` + +## When to Use Echo Components + +Use Echo Components when you want to: + +- **Speed up development** - Skip building common UI patterns from scratch +- **Ensure consistency** - Maintain design consistency across Echo applications +- **Focus on business logic** - Spend time on your app's unique features instead of UI +- **Leverage Echo integration** - Get built-in support for Echo's authentication and billing systems + +## Next Steps + +- **[Installation Guide](/docs/components/installation)** - Learn how to set up Echo Components in your project +- **[Echo Account Component](/docs/components/echo-account)** - Complete guide to the Echo Account component +- **[UI Components](/docs/components/ui-components)** - Overview of available UI components +- **[Customization](/docs/components/customization)** - How to customize and theme Echo Components \ No newline at end of file diff --git a/packages/app/control/docs/components/installation.mdx b/packages/app/control/docs/components/installation.mdx new file mode 100644 index 000000000..8f77eac8b --- /dev/null +++ b/packages/app/control/docs/components/installation.mdx @@ -0,0 +1,165 @@ +--- +title: Installation +description: How to install and set up Echo Components +--- + +# Installation + +This guide will walk you through installing Echo Components in your project using the Shadcn CLI. + +## Prerequisites + +Before installing Echo Components, make sure you have: + +- A React or Next.js project +- [Shadcn CLI](https://ui.shadcn.com/docs/installation) installed +- Echo SDK already set up in your project + +## Installing Components + +### Using Shadcn CLI + +Echo Components are distributed through our custom registry at [echo-components.com](https://echo-components.com/). You can install them using the Shadcn CLI: + +```bash +# Install Echo Account component for React +pnpm dlx shadcn@latest add https://echo-components.com/r/echo-account-react.json + +# Install Echo Account component for Next.js +pnpm dlx shadcn@latest add https://echo-components.com/r/echo-account-next.json +``` + +### Manual Installation + +If you prefer to install components manually, you can: + +1. Visit [echo-components.com](https://echo-components.com/) +2. Browse the available components +3. Copy the component code directly into your project +4. Install any required dependencies + +## Dependencies + +Echo Components require several dependencies that will be automatically installed: + +### Required Dependencies + +- `@radix-ui/react-popover` - For popover functionality +- `@radix-ui/react-skeleton` - For loading states +- `@radix-ui/react-avatar` - For user avatars +- `@radix-ui/react-tooltip` - For tooltips +- `autonumeric` - For money input formatting + +### Echo SDK Dependencies + +Make sure you have the appropriate Echo SDK installed: + +```bash +# For React projects +npm install @merit-systems/echo-react-sdk + +# For Next.js projects +npm install @merit-systems/echo-next-sdk +``` + +## Project Setup + +### 1. Configure Shadcn + +If you haven't already, initialize Shadcn in your project: + +```bash +npx shadcn@latest init +``` + +### 2. Install Base Dependencies + +Echo Components build on top of Shadcn's base components. Make sure you have the required base components: + +```bash +# Install required base components +npx shadcn@latest add popover +npx shadcn@latest add skeleton +npx shadcn@latest add avatar +npx shadcn@latest add input +npx shadcn@latest add tooltip +``` + +### 3. Set Up Echo Provider + +Make sure your Echo provider is properly configured in your app: + +```tsx +// For React apps +import { EchoProvider } from '@merit-systems/echo-react-sdk'; + +function App() { + return ( + + {/* Your app content */} + + ); +} +``` + +```tsx +// For Next.js apps +import { EchoProvider } from '@merit-systems/echo-next-sdk'; + +export default function RootLayout({ children }) { + return ( + + + + {children} + + + + ); +} +``` + +## Verification + +After installation, you can verify that Echo Components are working by adding a simple Echo Account component to your app: + +```tsx +import { EchoAccount } from '@/components/echo-account-react'; + +export default function MyApp() { + return ( +
+

My Echo App

+ +
+ ); +} +``` + +If the component renders without errors and shows the Echo account interface, your installation is successful! + +## Troubleshooting + +### Common Issues + +**Component not found errors:** +- Make sure you've installed the Echo SDK +- Verify that the Echo provider is wrapping your app +- Check that all required base components are installed + +**Styling issues:** +- Ensure Tailwind CSS is properly configured +- Check that your `tailwind.config.js` includes the component paths +- Verify that CSS variables for theming are set up + +**TypeScript errors:** +- Make sure TypeScript is configured to resolve the component paths +- Check that all type definitions are properly imported + +### Getting Help + +If you encounter issues: + +1. Check the [Echo Components Registry](https://echo-components.com/) for the latest component versions +2. Review the [Shadcn documentation](https://ui.shadcn.com/docs) for base component setup +3. Join our [Discord community](https://discord.gg/merit) for support \ No newline at end of file diff --git a/packages/app/control/docs/components/meta.json b/packages/app/control/docs/components/meta.json new file mode 100644 index 000000000..af4bafd74 --- /dev/null +++ b/packages/app/control/docs/components/meta.json @@ -0,0 +1,11 @@ +{ + "title": "Components", + "pages": [ + "index", + "installation", + "echo-account", + "ui-components", + "customization" + ], + "icon": "Component" + } \ No newline at end of file diff --git a/packages/app/control/docs/components/ui-components.mdx b/packages/app/control/docs/components/ui-components.mdx new file mode 100644 index 000000000..621efbaae --- /dev/null +++ b/packages/app/control/docs/components/ui-components.mdx @@ -0,0 +1,325 @@ +--- +title: UI Components +description: Available UI components in the Echo Components registry +--- + +# UI Components + +Echo Components includes a comprehensive set of UI components designed specifically for Echo applications. These components provide consistent design patterns and integrate seamlessly with the Echo ecosystem. + +## Available Components + +### Echo Button + +Enhanced button component with Echo-specific variants and styling. + +```tsx +import { Button } from '@/components/echo-button'; + +// Basic usage + + +// With variants + + +``` + +#### Variants + +- `default` - Standard button styling +- `destructive` - For dangerous actions +- `outline` - Outlined button +- `primaryOutline` - Primary color outline +- `destructiveOutline` - Destructive outline +- `secondary` - Secondary styling +- `ghost` - Minimal styling +- `primaryGhost` - Primary ghost variant +- `link` - Link-style button +- `success` - Success state styling +- `turbo` - Special gradient styling +- `turboSecondary` - Secondary turbo variant +- `unstyled` - No default styling + +#### Sizes + +- `default` - Standard size +- `xs` - Extra small +- `sm` - Small +- `lg` - Large +- `icon` - Icon button size +- `navbar` - Navbar-specific sizing + +### Money Input + +Specialized input component for handling monetary values with proper formatting and validation. + +```tsx +import { MoneyInput } from '@/components/money-input'; + +function PaymentForm() { + const [amount, setAmount] = useState(0); + + return ( + + ); +} +``` + +#### Features + +- **Auto-formatting** - Automatic number formatting with commas +- **Currency symbols** - Built-in dollar sign display +- **Decimal places** - Configurable decimal precision +- **Validation** - Built-in min/max value validation +- **Accessibility** - Full keyboard navigation support + +#### Props + +```tsx +interface MoneyInputProps { + setAmount: (amount: number) => void; + initialAmount?: number; + placeholder?: string; + prefixClassName?: string; + className?: string; + inputClassName?: string; + hideDollarSign?: boolean; + decimalPlaces?: number; +} +``` + +### Echo Logo + +Branded logo component with light and dark mode support. + +```tsx +import { EchoLogo } from '@/components/logo'; + +// Basic usage + + +// With custom styling + +``` + +#### Features + +- **Theme support** - Automatic light/dark mode switching +- **Responsive** - Scales appropriately for different screen sizes +- **Customizable** - Easy to style with Tailwind classes + +## Component Registry Structure + +The Echo Components registry is organized as follows: + +``` +registry/ +├── echo/ +│ ├── blocks/ +│ │ ├── echo-account-button/ +│ │ │ ├── echo-account-react.tsx +│ │ │ ├── echo-account-next.tsx +│ │ │ ├── echo-account.tsx +│ │ │ ├── echo-popover.tsx +│ │ │ ├── balance.tsx +│ │ │ └── top-up-button.tsx +│ │ └── lib/ +│ │ └── currency-utils.ts +│ └── ui/ +│ ├── echo-button.tsx +│ ├── money-input.tsx +│ └── logo.tsx +└── public/ + └── logo/ + ├── light.svg + └── dark.svg +``` + +## Installation + +### Individual Components + +You can install individual components using the Shadcn CLI: + +```bash +# Install specific components +npx shadcn@latest add https://echo-components.com/r/echo-button.json +npx shadcn@latest add https://echo-components.com/r/money-input.json +npx shadcn@latest add https://echo-components.com/r/logo.json +``` + +### Full Registry + +Install the complete Echo Components registry: + +```bash +# Install all Echo components +npx shadcn@latest add https://echo-components.com/r/echo-account-react.json +``` + +## Dependencies + +### Required Dependencies + +All Echo UI components require these base dependencies: + +```json +{ + "@radix-ui/react-slot": "^1.0.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.0.0", + "tailwind-merge": "^2.0.0" +} +``` + +### Component-Specific Dependencies + +Some components have additional requirements: + +**Money Input:** +```json +{ + "autonumeric": "^4.6.0" +} +``` + +**Echo Account:** +```json +{ + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-skeleton": "^1.0.3", + "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-tooltip": "^1.0.7" +} +``` + +## Styling and Theming + +### CSS Variables + +Echo Components use CSS variables for theming: + +```css +:root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + --secondary: 210 40% 96%; + --secondary-foreground: 222.2 47.4% 11.2%; + --muted: 210 40% 96%; + --muted-foreground: 215.4 16.3% 46.9%; + --accent: 210 40% 96%; + --accent-foreground: 222.2 47.4% 11.2%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 47.4% 11.2%; + --radius: 0.5rem; +} +``` + +### Custom Themes + +You can create custom themes by overriding CSS variables: + +```css +.dark-theme { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + /* ... other variables */ +} +``` + +## Best Practices + +### Performance + +- Use `React.memo` for components that don't need frequent re-renders +- Implement proper loading states for async operations +- Use `useCallback` and `useMemo` for expensive computations + +### Accessibility + +- Always provide proper ARIA labels +- Ensure keyboard navigation works correctly +- Test with screen readers +- Maintain proper color contrast ratios + +### Integration + +- Use Echo Components consistently across your app +- Follow the established design patterns +- Customize components through props rather than modifying source code +- Keep components up to date with the latest registry versions + +## Examples + +### Complete Form with Echo Components + +```tsx +import { Button } from '@/components/echo-button'; +import { MoneyInput } from '@/components/money-input'; +import { EchoAccount } from '@/components/echo-account-react'; + +function PaymentForm() { + const [amount, setAmount] = useState(0); + const [isProcessing, setIsProcessing] = useState(false); + + const handleSubmit = async () => { + setIsProcessing(true); + // Process payment + setIsProcessing(false); + }; + + return ( +
+ + +
+ + +
+ + +
+ ); +} +``` + +### Custom Button Variants + +```tsx +import { Button } from '@/components/echo-button'; + +function CustomButtons() { + return ( +
+ + + + + +
+ ); +} +``` \ No newline at end of file diff --git a/packages/app/control/docs/meta.json b/packages/app/control/docs/meta.json index 44a90e05f..2b36938ad 100644 --- a/packages/app/control/docs/meta.json +++ b/packages/app/control/docs/meta.json @@ -11,7 +11,8 @@ "money", "advanced", "cookbook", - "x402" + "x402", + "components" ], "root": true } From 00c50bb237b5f8a9f56c481d5cd6b22736bdcb1c Mon Sep 17 00:00:00 2001 From: Dhaval Chaudhari Date: Wed, 22 Oct 2025 22:25:28 +0530 Subject: [PATCH 12/38] fix imports --- packages/app/control/docs/components/customization.mdx | 1 + packages/app/control/docs/components/echo-account.mdx | 1 + packages/app/control/docs/components/ui-components.mdx | 2 ++ 3 files changed, 4 insertions(+) diff --git a/packages/app/control/docs/components/customization.mdx b/packages/app/control/docs/components/customization.mdx index 10f874cbd..d9927b198 100644 --- a/packages/app/control/docs/components/customization.mdx +++ b/packages/app/control/docs/components/customization.mdx @@ -103,6 +103,7 @@ Create your own color schemes: ```tsx import { Button, buttonVariants } from '@/components/echo-button'; import { cn } from '@/lib/utils'; +import { cva } from 'class-variance-authority'; // Custom variant const customButtonVariants = cva( diff --git a/packages/app/control/docs/components/echo-account.mdx b/packages/app/control/docs/components/echo-account.mdx index 698c3870b..014d480f4 100644 --- a/packages/app/control/docs/components/echo-account.mdx +++ b/packages/app/control/docs/components/echo-account.mdx @@ -73,6 +73,7 @@ The main button that triggers the account popover: ```tsx import { EchoAccountButton } from '@/components/echo-account-button/echo-account'; +import { useEcho } from '@merit-systems/echo-react-sdk'; // Usage with Echo context const echo = useEcho(); diff --git a/packages/app/control/docs/components/ui-components.mdx b/packages/app/control/docs/components/ui-components.mdx index 621efbaae..84936a4d8 100644 --- a/packages/app/control/docs/components/ui-components.mdx +++ b/packages/app/control/docs/components/ui-components.mdx @@ -55,6 +55,7 @@ Specialized input component for handling monetary values with proper formatting ```tsx import { MoneyInput } from '@/components/money-input'; +import { useState } from 'react'; function PaymentForm() { const [amount, setAmount] = useState(0); @@ -268,6 +269,7 @@ You can create custom themes by overriding CSS variables: import { Button } from '@/components/echo-button'; import { MoneyInput } from '@/components/money-input'; import { EchoAccount } from '@/components/echo-account-react'; +import { useState } from 'react'; function PaymentForm() { const [amount, setAmount] = useState(0); From 51dedc6b366c78191aed446ff2b5478038011aea Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Wed, 22 Oct 2025 15:02:48 -0400 Subject: [PATCH 13/38] relaxing env validation locally --- packages/app/control/.env.example | 4 ++ packages/app/control/src/env.ts | 18 +++----- packages/app/control/src/instrumentation.ts | 13 ++++++ packages/app/control/src/logger.ts | 45 +++++++++++-------- .../control/src/services/email/lib/client.ts | 4 +- .../control/src/services/email/lib/send.ts | 15 ++++++- .../app/control/src/services/stripe/client.ts | 8 ++-- .../src/services/stripe/create-link/lib.ts | 4 ++ .../stripe/webhook/construct-event.ts | 4 ++ 9 files changed, 80 insertions(+), 35 deletions(-) diff --git a/packages/app/control/.env.example b/packages/app/control/.env.example index adb5a1743..8dabb238d 100644 --- a/packages/app/control/.env.example +++ b/packages/app/control/.env.example @@ -1,3 +1,7 @@ +# ---------- +# Environment +# ---------- +VERCEL_ENV="local" # ---------- # Application diff --git a/packages/app/control/src/env.ts b/packages/app/control/src/env.ts index 247102fc1..a6905a43a 100644 --- a/packages/app/control/src/env.ts +++ b/packages/app/control/src/env.ts @@ -56,21 +56,15 @@ export const env = createEnv({ // email - AUTH_RESEND_KEY: !IS_INTEGRATION_TEST - ? z.string() - : z.string().default('auth-resend-key-change-in-production'), - AUTH_RESEND_FROM_EMAIL: !IS_INTEGRATION_TEST - ? z.email() - : z.string().default('john@doe.com'), - RESEND_FLOW_CONTROL_KEY: IS_STRICT - ? z.string() - : z.string().default('resend-flow-control-key'), + AUTH_RESEND_KEY: IS_STRICT ? z.string() : z.string().optional(), + AUTH_RESEND_FROM_EMAIL: IS_STRICT ? z.email() : z.string().optional(), + RESEND_FLOW_CONTROL_KEY: IS_STRICT ? z.string() : z.string().optional(), // stripe - STRIPE_SECRET_KEY: z.string(), - STRIPE_PUBLISHABLE_KEY: z.string(), - STRIPE_WEBHOOK_SECRET: z.string(), + STRIPE_SECRET_KEY: IS_STRICT ? z.string() : z.string().optional(), + STRIPE_PUBLISHABLE_KEY: IS_STRICT ? z.string() : z.string().optional(), + STRIPE_WEBHOOK_SECRET: IS_STRICT ? z.string() : z.string().optional(), WEBHOOK_URL: IS_STRICT ? z.url() : z.url().default('http://localhost:3000/stripe/webhook'), diff --git a/packages/app/control/src/instrumentation.ts b/packages/app/control/src/instrumentation.ts index 7c19c87df..c9e4d2aa4 100644 --- a/packages/app/control/src/instrumentation.ts +++ b/packages/app/control/src/instrumentation.ts @@ -7,7 +7,20 @@ const SIGNOZ_INGESTION_KEY = env.SIGNOZ_INGESTION_KEY; const OTEL_EXPORTER_OTLP_ENDPOINT = env.OTEL_EXPORTER_OTLP_ENDPOINT; const SIGNOZ_SERVICE_NAME = env.SIGNOZ_SERVICE_NAME; +// Check if telemetry config is properly set +const isTelemetryConfigured = + OTEL_EXPORTER_OTLP_ENDPOINT && + OTEL_EXPORTER_OTLP_ENDPOINT !== 'undefined' && + SIGNOZ_SERVICE_NAME && + SIGNOZ_SERVICE_NAME !== 'undefined'; + export function register() { + // Skip telemetry setup if config is missing or invalid + if (!isTelemetryConfigured) { + console.log('[Instrumentation] Skipping telemetry setup (config missing)'); + return; + } + // --- Traces --- registerOTel({ serviceName: SIGNOZ_SERVICE_NAME, diff --git a/packages/app/control/src/logger.ts b/packages/app/control/src/logger.ts index 350b33dcd..f7ad45396 100644 --- a/packages/app/control/src/logger.ts +++ b/packages/app/control/src/logger.ts @@ -125,38 +125,47 @@ class ConsoleLogProcessor implements LogRecordProcessor { // --- Setup function --- export function setupLoggerProvider() { - const logExporter = new OTLPLogExporter({ - url: `${OTEL_EXPORTER_OTLP_ENDPOINT}/v1/logs`, - headers: SIGNOZ_INGESTION_KEY - ? { - 'signoz-access-token': SIGNOZ_INGESTION_KEY, - } - : {}, - }); + // Check if telemetry config is properly set + const isTelemetryConfigured = + OTEL_EXPORTER_OTLP_ENDPOINT && + OTEL_EXPORTER_OTLP_ENDPOINT !== 'undefined' && + SIGNOZ_SERVICE_NAME && + SIGNOZ_SERVICE_NAME !== 'undefined'; const resource = new Resource({ - 'service.name': SIGNOZ_SERVICE_NAME, + 'service.name': SIGNOZ_SERVICE_NAME || 'echo-control-local', 'service.environment': NODE_ENV, }); const loggerProvider = new LoggerProvider({ resource }); - const batchProcessor = new BatchLogRecordProcessor(logExporter); - // Add the OTLP processor with trace context injection - loggerProvider.addLogRecordProcessor( - new TraceContextLogProcessor(batchProcessor) - ); - - // Add console processor for local development and debugging - // You can control this with an environment variable if needed + // Only set up OTLP exporter if telemetry is properly configured + if (isTelemetryConfigured) { + const logExporter = new OTLPLogExporter({ + url: `${OTEL_EXPORTER_OTLP_ENDPOINT}/v1/logs`, + headers: SIGNOZ_INGESTION_KEY + ? { + 'signoz-access-token': SIGNOZ_INGESTION_KEY, + } + : {}, + }); + + const batchProcessor = new BatchLogRecordProcessor(logExporter); + + // Add the OTLP processor with trace context injection + loggerProvider.addLogRecordProcessor( + new TraceContextLogProcessor(batchProcessor) + ); + } + // Always add console processor for local development and debugging loggerProvider.addLogRecordProcessor( new TraceContextLogProcessor(new ConsoleLogProcessor()) ); logs.setGlobalLoggerProvider(loggerProvider); - return logs.getLogger(SIGNOZ_SERVICE_NAME); // handy to export directly + return logs.getLogger(SIGNOZ_SERVICE_NAME || 'echo-control-local'); } export const logger = setupLoggerProvider(); diff --git a/packages/app/control/src/services/email/lib/client.ts b/packages/app/control/src/services/email/lib/client.ts index c3a968aff..96a7d8d02 100644 --- a/packages/app/control/src/services/email/lib/client.ts +++ b/packages/app/control/src/services/email/lib/client.ts @@ -2,4 +2,6 @@ import { Resend } from 'resend'; import { env } from '@/env'; -export const emailClient = new Resend(env.AUTH_RESEND_KEY); +export const emailClient = env.AUTH_RESEND_KEY + ? new Resend(env.AUTH_RESEND_KEY) + : null; diff --git a/packages/app/control/src/services/email/lib/send.ts b/packages/app/control/src/services/email/lib/send.ts index cda1bfc36..cc73ea589 100644 --- a/packages/app/control/src/services/email/lib/send.ts +++ b/packages/app/control/src/services/email/lib/send.ts @@ -12,6 +12,18 @@ export function sendEmailWithRetry( options?: CreateEmailRequestOptions, config?: Partial ) { + // Skip email sending if not in production environment + if (!emailClient || !env.AUTH_RESEND_FROM_EMAIL) { + console.log( + '[Email Skipped - Not in Production] Would have sent email:', + payload + ); + return Promise.resolve({ + data: { id: 'skipped-not-in-production' }, + error: null, + } as ResendResult<{ id: string }>); + } + const idempotencyKey = options?.idempotencyKey ?? randomUUID(); const stableOptions: CreateEmailRequestOptions = { ...options, @@ -19,10 +31,11 @@ export function sendEmailWithRetry( }; const fromEmail = env.AUTH_RESEND_FROM_EMAIL; + const client = emailClient; // TypeScript refinement return resendRetry( () => - emailClient.emails.send( + client.emails.send( { ...payload, from: `Sam Ragsdale <${fromEmail}>` as const, diff --git a/packages/app/control/src/services/stripe/client.ts b/packages/app/control/src/services/stripe/client.ts index 542b5cba6..c19273a8e 100644 --- a/packages/app/control/src/services/stripe/client.ts +++ b/packages/app/control/src/services/stripe/client.ts @@ -2,6 +2,8 @@ import Stripe from 'stripe'; import { env } from '@/env'; -export const stripe = new Stripe(env.STRIPE_SECRET_KEY, { - apiVersion: '2025-05-28.basil', -}); +export const stripe = env.STRIPE_SECRET_KEY + ? new Stripe(env.STRIPE_SECRET_KEY, { + apiVersion: '2025-05-28.basil', + }) + : null; diff --git a/packages/app/control/src/services/stripe/create-link/lib.ts b/packages/app/control/src/services/stripe/create-link/lib.ts index 7a06d0f13..d5956ef21 100644 --- a/packages/app/control/src/services/stripe/create-link/lib.ts +++ b/packages/app/control/src/services/stripe/create-link/lib.ts @@ -22,6 +22,10 @@ export const createPaymentLink = async ( userId: string, parameters: z.infer ) => { + if (!stripe) { + throw new Error('Stripe is not configured for this environment'); + } + const { amount, name, description, successUrl, metadata } = createPaymentLinkSchema.parse(parameters); diff --git a/packages/app/control/src/services/stripe/webhook/construct-event.ts b/packages/app/control/src/services/stripe/webhook/construct-event.ts index 949d4e373..9f75aa456 100644 --- a/packages/app/control/src/services/stripe/webhook/construct-event.ts +++ b/packages/app/control/src/services/stripe/webhook/construct-event.ts @@ -6,6 +6,10 @@ import { logger } from '@/logger'; import type { NextRequest } from 'next/server'; export const constructStripeEvent = (request: NextRequest, body: string) => { + if (!stripe || !env.STRIPE_WEBHOOK_SECRET) { + throw new Error('Stripe is not configured for this environment'); + } + const signature = request.headers.get('stripe-signature'); if (!signature) { From 832a467346e3210523742de6afff18c4e2d13d7f Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Wed, 22 Oct 2025 15:05:51 -0400 Subject: [PATCH 14/38] formatting --- .../server/src/services/fund-repo/fundRepoService.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/app/server/src/services/fund-repo/fundRepoService.ts b/packages/app/server/src/services/fund-repo/fundRepoService.ts index 9fd3acf0f..6368fac47 100644 --- a/packages/app/server/src/services/fund-repo/fundRepoService.ts +++ b/packages/app/server/src/services/fund-repo/fundRepoService.ts @@ -112,11 +112,7 @@ export async function fundRepo( } } - - -export async function safeFundRepo( - amount: number, -): Promise { +export async function safeFundRepo(amount: number): Promise { try { const repoId = process.env.MERIT_REPO_ID; if (!repoId) { @@ -124,6 +120,8 @@ export async function safeFundRepo( } await fundRepo(amount, Number(repoId)); } catch (error) { - logger.error(`Error in safe funding repo: ${error instanceof Error ? error.message : 'Unknown error'} | Amount: ${amount}`); + logger.error( + `Error in safe funding repo: ${error instanceof Error ? error.message : 'Unknown error'} | Amount: ${amount}` + ); } -} \ No newline at end of file +} From 30ecdb3964546a2a1a2905ed3a57f53f668b5ad2 Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Wed, 22 Oct 2025 15:28:29 -0400 Subject: [PATCH 15/38] resend lint error --- packages/app/control/src/services/email/queue.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/app/control/src/services/email/queue.ts b/packages/app/control/src/services/email/queue.ts index c278c98b3..5655a61da 100644 --- a/packages/app/control/src/services/email/queue.ts +++ b/packages/app/control/src/services/email/queue.ts @@ -33,11 +33,13 @@ export const queueJob = async (body: z.infer) => { type: 'email', job: body, }, - flowControl: { - key: env.RESEND_FLOW_CONTROL_KEY, - rate: 2, - period: '1m', - }, + ...(env.RESEND_FLOW_CONTROL_KEY && { + flowControl: { + key: env.RESEND_FLOW_CONTROL_KEY, + rate: 2, + period: '1m', + }, + }), }); }; From 1ebf7c929d595387383c2bebeb14c9f2304c5a84 Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Thu, 23 Oct 2025 11:06:35 -0400 Subject: [PATCH 16/38] test user locally --- .../src/app/(auth)/(control)/login/page.tsx | 29 +++++++++++++++++++ packages/app/control/src/auth/config.ts | 17 ++++++++++- .../app/control/src/services/email/queue.ts | 5 ++++ 3 files changed, 50 insertions(+), 1 deletion(-) diff --git a/packages/app/control/src/app/(auth)/(control)/login/page.tsx b/packages/app/control/src/app/(auth)/(control)/login/page.tsx index f1a1effb4..26836d165 100644 --- a/packages/app/control/src/app/(auth)/(control)/login/page.tsx +++ b/packages/app/control/src/app/(auth)/(control)/login/page.tsx @@ -13,6 +13,9 @@ import { oauthProviders } from '@/auth/providers'; import { cn } from '@/lib/utils'; import type { Route } from 'next'; +import { env } from '@/env'; + +const IS_LOCAL_MODE = env.NODE_ENV === 'development'; export default async function SignInPage({ searchParams, @@ -120,6 +123,32 @@ export default async function SignInPage({ + {IS_LOCAL_MODE && ( + <> +
+ + dev only + +
+
{ + 'use server'; + await signIn('test-user-1', { + redirectTo: redirectTo || '/', + }); + }} + className="w-full" + > + +
+ + )} ); } diff --git a/packages/app/control/src/auth/config.ts b/packages/app/control/src/auth/config.ts index ab51f30d0..eab941c10 100644 --- a/packages/app/control/src/auth/config.ts +++ b/packages/app/control/src/auth/config.ts @@ -27,8 +27,23 @@ declare module 'next-auth/jwt' { const IS_TEST_MODE = env.INTEGRATION_TEST_MODE; +const IS_LOCAL_MODE = env.NODE_ENV === 'development'; + +// Determine which providers to use based on environment +const getProviders = () => { + if (IS_TEST_MODE) { + return testProviders; + } + + if (IS_LOCAL_MODE) { + return [...testProviders, ...oauthProviders]; + } + + return oauthProviders; +}; + export const authConfig = { - providers: IS_TEST_MODE ? testProviders : oauthProviders, + providers: getProviders(), // Only allow skipCSRFCheck in test mode skipCSRFCheck: IS_TEST_MODE ? skipCSRFCheck : undefined, pages: { diff --git a/packages/app/control/src/services/email/queue.ts b/packages/app/control/src/services/email/queue.ts index c278c98b3..6081066b1 100644 --- a/packages/app/control/src/services/email/queue.ts +++ b/packages/app/control/src/services/email/queue.ts @@ -27,6 +27,11 @@ export const emailJobSchema = z.discriminatedUnion('campaign', [ ]); export const queueJob = async (body: z.infer) => { + // Skip queueing in local development mode + if (env.NODE_ENV === 'development' || !env.RESEND_FLOW_CONTROL_KEY) { + return; + } + await queueClient.publishJSON({ url: `${env.NEXT_PUBLIC_APP_URL}/api/jobs`, body: { From f425d59fb2e426c592b9cc220bac1adddf9233f5 Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Thu, 23 Oct 2025 11:08:25 -0400 Subject: [PATCH 17/38] test user clean up --- packages/app/control/src/services/email/queue.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/app/control/src/services/email/queue.ts b/packages/app/control/src/services/email/queue.ts index d62f7a26f..6081066b1 100644 --- a/packages/app/control/src/services/email/queue.ts +++ b/packages/app/control/src/services/email/queue.ts @@ -38,13 +38,11 @@ export const queueJob = async (body: z.infer) => { type: 'email', job: body, }, - ...(env.RESEND_FLOW_CONTROL_KEY && { - flowControl: { - key: env.RESEND_FLOW_CONTROL_KEY, - rate: 2, - period: '1m', - }, - }), + flowControl: { + key: env.RESEND_FLOW_CONTROL_KEY, + rate: 2, + period: '1m', + }, }); }; From 379a75ffac847eba069e92d341305817931e907d Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Thu, 23 Oct 2025 11:17:28 -0400 Subject: [PATCH 18/38] cleaning up env --- packages/app/control/.env.example | 3 +- packages/app/control/README.md | 29 ++++++----------- .../src/app/(auth)/(control)/login/page.tsx | 2 +- packages/app/control/src/auth/providers.ts | 31 +++++++++++++++++++ 4 files changed, 43 insertions(+), 22 deletions(-) diff --git a/packages/app/control/.env.example b/packages/app/control/.env.example index 8dabb238d..fdde53e31 100644 --- a/packages/app/control/.env.example +++ b/packages/app/control/.env.example @@ -1,7 +1,8 @@ # ---------- # Environment # ---------- -VERCEL_ENV="local" + +SKIP_ENV_VALIDATION=1 # Remove in production! # ---------- # Application diff --git a/packages/app/control/README.md b/packages/app/control/README.md index c507343d7..446a27a46 100644 --- a/packages/app/control/README.md +++ b/packages/app/control/README.md @@ -73,39 +73,28 @@ A comprehensive Next.js application for managing Echo applications, API keys, an # Example: DATABASE_URL="postgresql://username:password@localhost:5469/echo_control" ``` -4. **Run database migrations**: +4. ** Create auth secret**: + + ```bash + pnpm dlx auth secret + ``` + +5. **Run database migrations**: ```bash npx prisma generate npx prisma db push ``` -5. **Start the development server**: +6. **Start the development server**: ```bash pnpm run dev ``` -6. **Open the application**: +7. **Open the application**: Visit [http://localhost:3000](http://localhost:3000) -## Environment Variables - -Create a `.env` file with the following variables: - -```env -# Database -DATABASE_URL="postgresql://username:password@localhost:5469/echo_control" - -# Stripe (Mocked) -STRIPE_SECRET_KEY="mock_stripe_secret_key" -STRIPE_PUBLISHABLE_KEY="mock_stripe_publishable_key" -STRIPE_WEBHOOK_SECRET="mock_webhook_secret" - -# Application -NEXTAUTH_URL="http://localhost:3000" -API_KEY_PREFIX="echo_" -``` ## Features Overview diff --git a/packages/app/control/src/app/(auth)/(control)/login/page.tsx b/packages/app/control/src/app/(auth)/(control)/login/page.tsx index 26836d165..5601a6b1d 100644 --- a/packages/app/control/src/app/(auth)/(control)/login/page.tsx +++ b/packages/app/control/src/app/(auth)/(control)/login/page.tsx @@ -133,7 +133,7 @@ export default async function SignInPage({
{ 'use server'; - await signIn('test-user-1', { + await signIn('local-user', { redirectTo: redirectTo || '/', }); }} diff --git a/packages/app/control/src/auth/providers.ts b/packages/app/control/src/auth/providers.ts index cbbd3dafb..302eb54aa 100644 --- a/packages/app/control/src/auth/providers.ts +++ b/packages/app/control/src/auth/providers.ts @@ -124,6 +124,37 @@ export const testProviders: Provider[] = [ image: 'http://echo.merit.systems/logo/light.svg', }); + return { + id: user.id, + name: user.name, + email: user.email, + image: user.image, + }; + }, + }), + Credentials({ + id: 'local-user', + name: 'Local User', + credentials: {}, + authorize: async () => { + const existingUser = await getUserByEmail('local@example.com'); + + if (existingUser) { + return { + id: existingUser.id, + name: existingUser.name, + email: existingUser.email, + image: existingUser.image, + }; + } + + const user = await createUser({ + id: 'ffffffff-ffff-ffff-ffff-ffffffffffff', + name: 'Local User', + email: 'local@example.com', + image: 'http://echo.merit.systems/logo/light.svg', + }); + return { id: user.id, name: user.name, From a2d0dde99a9456af26b7bda9b186f9706b76febd Mon Sep 17 00:00:00 2001 From: Mason Hall Date: Thu, 23 Oct 2025 11:26:07 -0400 Subject: [PATCH 19/38] contributing.md --- CONTRIBUTING.md | 519 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 519 insertions(+) create mode 100644 CONTRIBUTING.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..aed1a636c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,519 @@ +# Contributing to Echo + +Thank you for your interest in contributing to Echo! We're building user-pays AI infrastructure that helps developers monetize their AI applications without fronting costs. Every contribution helps make AI development more accessible and sustainable. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Bounties](#bounties) +- [Ways to Contribute](#ways-to-contribute) +- [Getting Started](#getting-started) +- [Development Workflow](#development-workflow) +- [Project Structure](#project-structure) +- [Coding Standards](#coding-standards) +- [Commit Message Guidelines](#commit-message-guidelines) +- [Pull Request Process](#pull-request-process) +- [Testing](#testing) +- [Documentation](#documentation) +- [Community and Support](#community-and-support) + +## Code of Conduct + +By participating in this project, you agree to be respectful, inclusive, and constructive. We aim to create a welcoming environment for all contributors regardless of background or experience level. + +## Bounties + +**Get paid to contribute to Echo!** + +We offer bounties for specific features, bug fixes, and improvements. This is a great way to get started with open source contributions while earning rewards for your work. + +[**View available bounties →**](https://terminal.merit.systems/Merit-Systems/echo/bounties) + +To claim a bounty: +1. Check the [bounties page](https://terminal.merit.systems/Merit-Systems/echo/bounties) for available tasks +2. Comment on the bounty to express interest +3. Submit your work via pull request +4. Receive payment once your PR is merged + +New bounties are added regularly, so check back often! + +## Ways to Contribute + +### 🐛 Report Bugs + +If you find a bug, please [open an issue](https://github.com/Merit-Systems/echo/issues/new) with: +- A clear, descriptive title +- Steps to reproduce the issue +- Expected vs. actual behavior +- Your environment (OS, Node version, browser, etc.) +- Screenshots or code snippets if applicable + +### 💡 Suggest Features + +Have an idea? We'd love to hear it! [Open a feature request](https://github.com/Merit-Systems/echo/issues/new) with: +- A clear description of the feature +- Use cases and examples +- Why this would benefit Echo users +- Any implementation ideas you might have + +### 📝 Improve Documentation + +Documentation improvements are always welcome! This includes: +- Fixing typos or unclear explanations +- Adding examples or tutorials +- Improving API documentation +- Translating documentation + +### 🔧 Submit Code Changes + +Ready to code? Check our [open issues](https://github.com/Merit-Systems/echo/issues) for ideas, or propose your own changes. + +## Getting Started + +### Prerequisites + +- **Node.js**: 18.0.0 or higher +- **pnpm**: 10.0.0 or higher +- **PostgreSQL**: Required for running Echo Control locally +- **Git**: For version control + +### Initial Setup + +1. **Fork and clone the repository** + + ```bash + git clone https://github.com/YOUR_USERNAME/echo.git + cd echo + ``` + +2. **Install dependencies** + + ```bash + pnpm install + ``` + +3. **Set up environment variables** + + For Echo Control: + ```bash + cd packages/app/control + cp .env.example .env + # Edit .env with your configuration + ``` + + For Echo Server: + ```bash + cd packages/app/server + cp .env.example .env + # Edit .env with your configuration + ``` + +4. **Set up the database** (for Echo Control) + + ```bash + cd packages/app/control + ./setup-db.sh + # Or manually: + npx prisma generate + npx prisma db push + ``` + +5. **Start development servers** + + From the root directory: + ```bash + pnpm dev + ``` + + This starts both Echo Control (localhost:3000) and Echo Server simultaneously. + +## Development Workflow + +### Creating a Branch + +Use descriptive branch names with prefixes: + +```bash +git checkout -b feature/add-anthropic-support +git checkout -b fix/balance-calculation-error +git checkout -b docs/improve-quickstart +git checkout -b refactor/simplify-auth-flow +``` + +### Making Changes + +1. Make your changes in focused, logical commits +2. Write or update tests for your changes +3. Ensure all tests pass: `pnpm test:all` +4. Run linting: `pnpm lint` +5. Check types: `pnpm type-check` +6. Format code: `pnpm format` + +### Testing Your Changes + +```bash +# Run all tests +pnpm test:all + +# Run unit tests only +pnpm test:unit + +# Run integration tests +pnpm test:integration + +# Test in a specific package +pnpm --filter @merit-systems/echo-react-sdk test +``` + +## Project Structure + +Echo is a monorepo organized as follows: + +``` +echo/ +├── packages/ +│ ├── app/ +│ │ ├── control/ # Echo Control Plane (Next.js app) +│ │ └── server/ # Echo Server (Express proxy) +│ ├── sdk/ +│ │ ├── ts/ # Core TypeScript SDK +│ │ ├── react/ # React SDK +│ │ ├── next/ # Next.js SDK +│ │ ├── aix402/ # AI SDK 402 payment protocol +│ │ └── auth-js-provider/# Auth.js provider +│ └── tests/ # Integration and smoke tests +├── templates/ # Starter templates +└── docs/ # Documentation +``` + +### Key Packages + +- **Echo Control** (`packages/app/control`): User-facing dashboard, authentication, billing +- **Echo Server** (`packages/app/server`): Proxy server for LLM requests with metering +- **Echo TS SDK** (`packages/sdk/ts`): Foundation for all framework-specific SDKs +- **Echo React SDK** (`packages/sdk/react`): React hooks and components +- **Echo Next SDK** (`packages/sdk/next`): Next.js App Router integration + +## Coding Standards + +### TypeScript + +- Use TypeScript for all new code +- Prefer explicit types over `any` +- Use interfaces for public APIs, types for internal structures +- Enable strict mode in `tsconfig.json` + +### Imports + +- **Always use absolute imports** with the `@` syntax, not relative imports +- Use kebab-case for TypeScript files +- Group imports: external packages → internal packages → local files + +```typescript +// ✅ Good +import { useState } from 'react'; +import { generateText } from 'ai'; + +import { useEchoAuth } from '@/hooks/use-echo-auth'; +import { Button } from '@/components/button'; + +// ❌ Bad +import { Button } from '../../../components/button'; +``` + +### React Components + +- Use functional components with hooks +- Prefer named exports for components +- Use TypeScript for prop types +- Follow kebab-case for component filenames + +```typescript +// user-avatar.tsx +export function UserAvatar({ user }: UserAvatarProps) { + // Implementation +} +``` + +### Naming Conventions + +- **Files**: kebab-case (`user-profile.tsx`, `api-client.ts`) +- **Components**: PascalCase (`UserProfile`, `ApiKeyList`) +- **Functions/Variables**: camelCase (`getUserBalance`, `apiKey`) +- **Constants**: SCREAMING_SNAKE_CASE (`API_KEY_PREFIX`, `MAX_RETRIES`) +- **Types/Interfaces**: PascalCase (`UserProfile`, `ApiResponse`) + +### Feature Flags + +When working with feature flags: + +- Use as few places as possible to reduce undefined behavior +- Store flag names in an enum (TypeScript) or const object (JavaScript) +- Use SCREAMING_SNAKE_CASE for flag names +- Gate flag-dependent code with validation checks + +```typescript +// ✅ Good +enum FeatureFlags { + ANTHROPIC_SUPPORT = 'anthropic_support', + USAGE_ANALYTICS = 'usage_analytics', +} + +if (flags[FeatureFlags.ANTHROPIC_SUPPORT] === true) { + // Feature-specific code +} +``` + +### Custom Properties (Analytics) + +- Store event/property names in enums or const objects when used in 2+ places +- Follow existing naming conventions (consult maintainers if unsure) +- Never change existing event names without discussion (breaks analytics) + +### Code Quality + +- Follow DRY (Don't Repeat Yourself) principles where appropriate +- Write self-documenting code with clear variable names +- Add comments for complex logic or non-obvious decisions +- Keep functions small and focused on a single responsibility +- Handle errors appropriately with meaningful messages + +## Commit Message Guidelines + +We follow [Conventional Commits](https://www.conventionalcommits.org/) for clear, semantic commit history. + +### Format + +``` +(): + + + +